@claudebernard/fhir-mapper
Version:
A simple FHIR / BCB resource mapper to help stay interoperable while still using the Claude Bernard intelligence
1,196 lines (1,187 loc) • 61.5 kB
JavaScript
const unitsOfTime = {
's': { value: 0, label: 'seconde' },
'min': { value: 10, label: 'minute' },
'h': { value: 20, label: 'heure' },
'd': { value: 40, label: 'jour' },
'wk': { value: 50, label: 'semaine' },
'mo': { value: 70, label: 'mois' },
'a': { value: 100, label: 'an' }
};
/**
* Converts a CB Posology Bean to BCB Posologie Structuree2 format
* Based on the Java constructor BCBPosologieStructuree2(PosologyBean poso)
* Maps all fields from the Java constructor
*/
function convertCBToBCB(poso) {
return {
// Direct mappings from the Java constructor - exact field mapping
idProduit: poso.productId,
codeTerrain: poso.code,
codeIndication: poso.indicationCode,
codeNature: poso.typeCode,
codeVoie: poso.routeCode,
codeProfil: poso.profilCode,
quantite1: poso.quantity1,
quantite2: poso.quantity2,
codeUnitePrise: poso.intakeUnitCode,
parKilo: poso.perKilo,
adequationUP: poso.adequacy,
codePar: poso.byCode,
combien1: poso.howMuch1,
combien2: poso.howMuch2,
tousLes: poso.every,
codeDuree1: poso.duration1Code,
codeDuree2: poso.duration2Code,
codeDuree3: poso.duration3Code,
codeMoment: poso.momentCode,
pendant1: poso.during1,
pendant2: poso.during2,
maximum: poso.maximum,
maximumPoids: poso.weightMaximum,
codeSpecifPrise1: poso.intakeSpecification1Code,
codeSpecifPrise2: poso.intakeSpecification2Code,
nbUnites: poso.numberOfUnits,
coeffMoment: poso.momentCoeff,
implicite: poso.implicit,
// Additional label fields for completeness (not in Java constructor but useful)
noPosologie: poso.patientPosology,
libellePosologie: poso.posologyLabel,
libIndication: poso.indicationLabel,
libVoie: poso.routeLabel,
libUnitePrise: poso.intakeUnitLabel,
libDuree1: poso.duration1Label,
libDuree2: poso.duration2Label,
libDuree3: poso.duration3Label,
libSpecifPrise1: poso.intakeSpecification1Label
};
}
/**
* Converts BCB Posologie Structuree2 back to CB Posology Bean format.
* This is the reverse of the convertCBToBCB function.
* @param bcbPosology BCB Posologie Structuree2 object
* @returns CB Posology Bean object
*/
function convertBCBToCB(bcbPosology) {
return {
// Reverse mapping from BCB to CB (Java constructor mapping in reverse)
productId: bcbPosology.idProduit ?? undefined,
code: bcbPosology.codeTerrain ?? undefined,
indicationCode: bcbPosology.codeIndication ?? undefined,
typeCode: bcbPosology.codeNature ?? undefined,
routeCode: bcbPosology.codeVoie ?? undefined,
profilCode: bcbPosology.codeProfil ?? undefined,
quantity1: bcbPosology.quantite1 ?? undefined,
quantity2: bcbPosology.quantite2 ?? undefined,
intakeUnitCode: bcbPosology.codeUnitePrise ?? undefined,
perKilo: bcbPosology.parKilo ?? undefined,
adequacy: bcbPosology.adequationUP ?? undefined,
byCode: bcbPosology.codePar ?? undefined,
howMuch1: bcbPosology.combien1 ?? undefined,
howMuch2: bcbPosology.combien2 ?? undefined,
every: bcbPosology.tousLes ?? undefined,
duration1Code: bcbPosology.codeDuree1 ?? undefined,
duration2Code: bcbPosology.codeDuree2 ?? undefined,
duration3Code: bcbPosology.codeDuree3 ?? undefined,
momentCode: bcbPosology.codeMoment ?? undefined,
during1: bcbPosology.pendant1 ?? undefined,
during2: bcbPosology.pendant2 ?? undefined,
maximum: bcbPosology.maximum ?? undefined,
weightMaximum: bcbPosology.maximumPoids ?? undefined,
intakeSpecification1Code: bcbPosology.codeSpecifPrise1 ?? undefined,
intakeSpecification2Code: bcbPosology.codeSpecifPrise2 ?? undefined,
numberOfUnits: bcbPosology.nbUnites ?? undefined,
momentCoeff: bcbPosology.coeffMoment ?? undefined,
implicit: bcbPosology.implicite ?? undefined,
// Label fields (reverse mapping)
patientPosology: bcbPosology.noPosologie ?? undefined,
posologyLabel: bcbPosology.libellePosologie ?? undefined,
indicationLabel: bcbPosology.libIndication ?? undefined,
routeLabel: bcbPosology.libVoie ?? undefined,
intakeUnitLabel: bcbPosology.libUnitePrise ?? undefined,
duration1Label: bcbPosology.libDuree1 ?? undefined,
duration2Label: bcbPosology.libDuree2 ?? undefined,
duration3Label: bcbPosology.libDuree3 ?? undefined,
intakeSpecification1Label: bcbPosology.libSpecifPrise1 ?? undefined
};
}
function buildDoseAndRate(dosage) {
const coding = {
system: dosage.codeUnitePrise ? 'https://platform.claudebernard.fr/fhir/CodeSystem/dosage-intake-units' : undefined,
code: dosage.codeUnitePrise ?? undefined,
unit: dosage.codeUnitePrise ? dosage.libUnitePrise : undefined
};
const doseAndRate = dosage.quantite2 ? {
doseRange: {
low: {
value: dosage.quantite1,
...coding
},
high: {
value: dosage.quantite2,
...coding
}
}
} : {
doseQuantity: {
value: dosage.quantite1,
...coding
}
};
return [doseAndRate];
}
// The formula used here is count = dose * frequency * (duration / period)
function calculateCount(dose, frequency, period, periodUnit, duration, durationUnit) {
// Provide default values for null or undefined parameters
const actualDose = dose ?? 0;
const actualFrequency = frequency ?? 0;
const actualPeriod = period ?? 1;
const actualDuration = duration ?? 0;
const actualPeriodUnit = periodUnit || 'd'; // Default to 'd' (days) if not provided
const actualDurationUnit = durationUnit || 'd'; // Default to 'd' (days) if not provided
// Convert duration to the same unit as period
let convertedDuration = actualDuration;
if (actualDurationUnit !== actualPeriodUnit && actualDuration && actualDurationUnit && actualPeriodUnit) {
convertedDuration = convertTime(actualDuration, actualDurationUnit, actualPeriodUnit);
}
// Check if dose, frequency, convertedDuration, or period is null or 0 and compute count accordingly
if (!actualDose) {
return actualFrequency * (convertedDuration / actualPeriod);
}
else if (!actualFrequency) {
return actualDose * (convertedDuration / actualPeriod);
}
else if (!convertedDuration) {
return actualDose * actualFrequency * actualPeriod;
}
else if (!actualPeriod) {
return actualDose * actualFrequency * convertedDuration;
}
else {
return actualDose * actualFrequency * (convertedDuration / actualPeriod);
}
}
function convertTime(value, fromUnit, toUnit) {
const conversionFactors = {
's': 1,
'min': 60,
'h': 3600,
'd': 86400,
'wk': 604800,
'mo': 2628000,
'a': 31536000
};
if (!conversionFactors[fromUnit]) {
throw new Error(`Invalid fromUnit: ${fromUnit}`);
}
if (!conversionFactors[toUnit]) {
throw new Error(`Invalid toUnit: ${toUnit}`);
}
// Convert the input value to seconds
const valueInSeconds = value * conversionFactors[fromUnit];
// Convert from seconds to the desired unit
const convertedValue = valueInSeconds / conversionFactors[toUnit];
return convertedValue;
}
async function handleCodifications(dosage, indicationMapper, routeMapper, intakeMapper) {
let conditionCode;
let conditionLabel;
let intakeCode;
let intakeLabel;
let routeCode;
let routeLabel;
let errors = [];
if (dosage.asNeededFor && dosage.asNeededFor[0].coding && dosage.asNeededFor[0].coding.length > 0) {
if (dosage.asNeededFor[0]?.coding?.[0]?.system === 'https://platform.claudebernard.fr/fhir/CodeSystem/amm-pathologies') {
conditionCode = dosage.asNeededFor?.[0]?.coding?.[0]?.code;
conditionLabel = dosage.asNeededFor?.[0]?.coding?.[0]?.display;
}
else if (indicationMapper) {
const result = await indicationMapper(dosage?.asNeededFor?.[0]?.coding?.[0]);
let indication = undefined;
if (Array.isArray(result)) {
if (result.length > 0)
indication = result[0];
}
else if (result) {
indication = result;
}
if (indication) {
conditionCode = indication.code;
conditionLabel = indication.label;
}
else {
conditionCode = undefined;
conditionLabel = undefined;
errors.push({
field: 'codeIndication',
message: 'No mapping found for indication code'
});
}
}
else {
errors.push({
field: 'codeIndication',
message: 'The system used in the asNeededFor coding field is not supported and no custom mapper was provided'
});
}
}
if (dosage.route && dosage.route.coding && dosage.route.coding.length > 0) {
if (dosage.route?.coding?.[0]?.system === 'https://platform.claudebernard.fr/fhir/CodeSystem/dosage-routes') {
routeCode = parseInt(dosage?.route?.coding?.[0]?.code ?? '');
routeLabel = dosage.route?.coding?.[0]?.display;
}
else if (dosage.route && routeMapper) {
const result = await routeMapper(dosage.route?.coding?.[0]);
let route = undefined;
if (Array.isArray(result)) {
if (result.length > 0)
route = result[0];
}
else if (result) {
route = result;
}
if (route) {
routeCode = parseInt(route.code);
routeLabel = route.label;
}
else {
routeCode = undefined;
routeLabel = undefined;
errors.push({
field: 'codeVoie',
message: 'No mapping found for route code'
});
}
}
else {
errors.push({
field: 'codeVoie',
message: 'The system used in the route coding field is not supported and no custom mapper was provided'
});
}
}
if (dosage.doseAndRate && dosage.doseAndRate.length > 0) {
if (dosage.doseAndRate?.[0]?.doseQuantity?.system === 'https://platform.claudebernard.fr/fhir/CodeSystem/dosage-intake-units') {
intakeCode = parseInt(dosage.doseAndRate?.[0]?.doseQuantity.code ?? '');
intakeLabel = dosage.doseAndRate?.[0]?.doseQuantity.unit;
}
else if (dosage.doseAndRate && intakeMapper) {
const result = await intakeMapper(dosage.doseAndRate?.[0]?.doseQuantity);
let intake = undefined;
if (Array.isArray(result)) {
if (result.length > 0)
intake = result[0];
}
else if (result) {
intake = result;
}
if (intake) {
intakeCode = parseInt(intake.code);
intakeLabel = intake.label;
}
else {
intakeCode = undefined;
intakeLabel = undefined;
errors.push({
field: 'codeUnitePrise',
message: 'No mapping found for intake code'
});
}
}
else {
errors.push({
field: 'codeUnitePrise',
message: 'The system used in the doseQuantity coding field is not supported and no custom mapper was provided'
});
}
}
return {
conditionCode,
conditionLabel,
intakeCode,
intakeLabel,
routeCode,
routeLabel,
codificationErrors: errors
};
}
// adding this function to support new naming convention without breaking changes
async function fhirToBcb$2(dosageInstructions, indicationMapper, routeMapper, intakeMapper) {
return await mapToBcb(dosageInstructions, indicationMapper, routeMapper, intakeMapper);
}
async function mapToBcb(dosageInstructions, indicationMapper, routeMapper, intakeMapper) {
if (!dosageInstructions || dosageInstructions.length === 0) {
return [
{
result: undefined,
errors: [{
field: 'root',
message: 'Invalid Dosage array'
}]
}
];
}
const safeIndicationMapper = wrapMaybeAsync$1(indicationMapper);
const safeRouteMapper = wrapMaybeAsync$1(routeMapper);
const safeIntakeMapper = wrapMaybeAsync$1(intakeMapper);
const results = [];
for (const dosage of dosageInstructions) {
let errors = [];
const periodUnit = dosage.timing?.repeat?.periodUnit;
const quantite1 = dosage?.doseAndRate?.[0]?.doseQuantity?.value ?? dosage?.doseAndRate?.[0]?.doseRange?.low?.value;
const quantite2 = dosage?.doseAndRate?.[0]?.doseRange?.high?.value ?? null;
let maximum = null;
let codeDuree3 = null;
let maxDosePerDay = null;
if (dosage.maxDosePerPeriod) {
maxDosePerDay = dosage.maxDosePerPeriod.find(period => period?.denominator?.unit === 'd' && period?.denominator?.value === 1);
if (maxDosePerDay) {
maximum = maxDosePerDay?.numerator?.value;
codeDuree3 = maxDosePerDay?.denominator?.unit;
}
}
let libSpecifPrise1 = dosage.asNeeded ? 'selon besoin' : null;
const doseSpacing = dosage.maxDosePerPeriod?.find(period => period?.denominator?.unit === 'h');
if (doseSpacing) {
const spacingText = `en espaçant les prises de ${doseSpacing?.denominator?.value}h minimum`;
libSpecifPrise1 = libSpecifPrise1 ? `${libSpecifPrise1}, ${spacingText}` : spacingText;
}
// handleCodifications must be updated to support async mappers
const { conditionCode, conditionLabel, intakeCode, intakeLabel, routeCode, routeLabel, codificationErrors } = await handleCodifications(dosage, safeIndicationMapper, safeRouteMapper, safeIntakeMapper);
const codeDuree2 = dosage?.timing?.repeat?.boundsDuration?.code;
const bcbDosage = {
noPosologie: dosage.sequence ?? undefined,
libellePosologie: dosage.text ?? undefined,
codeIndication: conditionCode,
libIndication: conditionLabel,
codeDuree1: periodUnit ? unitsOfTime[periodUnit]?.value : undefined,
libDuree1: periodUnit ? unitsOfTime[periodUnit]?.label : undefined,
quantite1: quantite1 ?? undefined,
quantite2: quantite2 ?? undefined,
tousLes: dosage.timing?.repeat?.period ?? undefined,
combien1: dosage.timing?.repeat?.frequency ?? undefined,
combien2: dosage.timing?.repeat?.frequencyMax ?? undefined,
pendant1: dosage.timing?.repeat?.boundsDuration?.value ?? undefined,
codeUnitePrise: intakeCode,
libUnitePrise: intakeLabel,
codeVoie: routeCode,
libVoie: routeLabel,
codeDuree2: codeDuree2 ? unitsOfTime[codeDuree2]?.value : undefined,
libDuree2: codeDuree2 ? unitsOfTime[codeDuree2]?.label : undefined,
maximum: maximum ?? undefined,
codeDuree3: codeDuree3 ? unitsOfTime[codeDuree3]?.value : undefined,
libDuree3: codeDuree3 ? unitsOfTime[codeDuree3]?.label : undefined,
libSpecifPrise1: libSpecifPrise1 ?? undefined
};
results.push({
result: bcbDosage,
errors: codificationErrors.concat(errors)
});
}
return results;
}
// adding this function to support new naming convention without breaking changes
function bcbToFhir$2(bcbDosages) {
return mapToFhir(bcbDosages);
}
function mapToFhir(bcbDosages) {
if (!bcbDosages || bcbDosages.length === 0) {
throw new Error('Invalid BCBPosologieStructuree2 array');
}
return bcbDosages.map((dosage) => {
const keys = Object.keys(unitsOfTime);
const periodUnit = keys.find(key => unitsOfTime[key].value === dosage.codeDuree1) ?? undefined;
const durationUnit = keys.find(key => unitsOfTime[key].value === dosage.codeDuree2) ?? undefined;
const maximumUnit = keys.find(key => unitsOfTime[key].value === dosage.codeDuree3) ?? undefined;
const route = dosage.codeVoie ? {
coding: [{
system: 'https://platform.claudebernard.fr/fhir/CodeSystem/dosage-routes',
code: dosage.codeVoie.toString(),
display: dosage.libVoie
}]
} : undefined;
const intake = dosage.codeUnitePrise ? {
coding: [{
system: 'https://platform.claudebernard.fr/fhir/CodeSystem/dosage-intake-units',
code: dosage.codeUnitePrise.toString(),
display: dosage.libUnitePrise
}]
} : undefined;
const indication = dosage.codeIndication ? [{
coding: [{
system: 'https://platform.claudebernard.fr/fhir/CodeSystem/amm-pathologies',
code: dosage.codeIndication,
display: dosage.libIndication
}]
}] : undefined;
const maxDosePerPeriod = dosage.maximum ? [{
numerator: {
value: dosage.maximum,
system: 'https://platform.claudebernard.fr/fhir/CodeSystem/dosage-intake-units',
code: intake?.coding?.[0]?.code,
unit: intake?.coding?.[0]?.display
},
denominator: {
value: 1,
unit: maximumUnit
}
}] : undefined;
const doseAndRate = buildDoseAndRate(dosage);
const boundsDuration = dosage.pendant1 ? { value: dosage.pendant1, unit: durationUnit } : undefined;
const asNeeded = dosage.libSpecifPrise1?.includes('besoin');
return {
result: {
text: dosage.libellePosologie,
sequence: dosage.noPosologie,
doseAndRate: doseAndRate,
maxDosePerPeriod: dosage.maximum ? maxDosePerPeriod : undefined,
route: route,
asNeeded: asNeeded,
asNeededFor: indication,
timing: {
repeat: {
frequency: dosage.combien1,
frequencyMax: dosage.combien2,
period: dosage.tousLes,
periodUnit: periodUnit,
boundsDuration: boundsDuration,
count: calculateCount(dosage.quantite1, dosage.combien1, dosage.tousLes, periodUnit, dosage.pendant1, durationUnit),
countMax: calculateCount(dosage.quantite2 ?? dosage.quantite1, dosage.combien2 ?? dosage.combien1, dosage.tousLes, periodUnit, dosage.pendant2 ?? dosage.pendant1, durationUnit)
}
}
},
errors: []
};
});
}
// Utility to wrap a possibly sync or async function so it always returns a Promise
function wrapMaybeAsync$1(fn) {
if (!fn)
return undefined;
return ((...args) => Promise.resolve(fn(...args)));
}
/**
* Converts FHIR Dosage instructions to CB Posology Bean format.
* Uses the existing fhirToBcb function and then converts BCB to CB format.
* @param dosageInstructions Array of FHIR Dosage resources
* @param indicationMapper Optional mapper function for indication codes
* @param routeMapper Optional mapper function for route codes
* @param intakeMapper Optional mapper function for intake codes
* @returns Promise of array of CB Posology Beans with mapping responses
*/
async function fhirToCb$1(dosageInstructions, indicationMapper, routeMapper, intakeMapper) {
// First convert FHIR to BCB using existing function
const bcbResults = await fhirToBcb$2(dosageInstructions, indicationMapper, routeMapper, intakeMapper);
// Convert each BCB result to CB format
const cbResults = [];
for (const bcbResult of bcbResults) {
if (bcbResult.result) {
// Convert BCB to CB format (reverse conversion)
const cbPosology = convertBCBToCB(bcbResult.result);
cbResults.push({
result: cbPosology,
errors: bcbResult.errors || []
});
}
else {
// If BCB conversion failed, pass through the errors
cbResults.push({
result: undefined,
errors: bcbResult.errors || [{
field: 'conversion',
message: 'Failed to convert FHIR to BCB format'
}]
});
}
}
return cbResults;
}
/**
* Converts CB Posology Bean format to FHIR Dosage instructions.
* Converts CB to BCB format first, then uses existing bcbToFhir function.
* @param cbPosologies Array of CB Posology Beans
* @returns Array of FHIR Dosage resources with mapping responses
*/
function cbToFhir$1(cbPosologies) {
if (!cbPosologies || cbPosologies.length === 0) {
return [{
result: undefined,
errors: [{
field: 'root',
message: 'Invalid CB Posology array'
}]
}];
}
// Convert each CB to BCB format using the converter
const bcbPosologies = [];
const conversionErrors = [];
for (const [index, cbPosology] of cbPosologies.entries()) {
try {
const bcbPosology = convertCBToBCB(cbPosology);
bcbPosologies.push(bcbPosology);
}
catch (error) {
conversionErrors.push({
field: `cbPosology[${index}]`,
message: `Failed to convert CB to BCB format: ${error}`
});
}
}
// If there were conversion errors, return them
if (conversionErrors.length > 0) {
return [{
result: undefined,
errors: conversionErrors
}];
}
// Use existing BCB to FHIR conversion
return bcbToFhir$2(bcbPosologies);
}
var dosageMapper = /*#__PURE__*/Object.freeze({
__proto__: null,
bcbToFhir: bcbToFhir$2,
cbToFhir: cbToFhir$1,
fhirToBcb: fhirToBcb$2,
fhirToCb: fhirToCb$1,
mapToBcb: mapToBcb,
mapToFhir: mapToFhir
});
/**
* Simple hash function compatible with both Node.js and browser environments
* This replaces the Node.js crypto module to avoid browser compatibility issues
*/
const simpleHash = (str) => {
let hash = 0;
if (str.length === 0)
return hash.toString(16).padStart(8, '0');
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // Convert to 32-bit integer
}
return Math.abs(hash).toString(16).padStart(8, '0').substring(0, 8);
};
/**
* Generate a deterministic 8-character hash from input data
* Compatible with browser and Node.js environments
*/
const generateHash = (data) => {
const dataString = typeof data === 'string' ? data : JSON.stringify(data);
return simpleHash(dataString);
};
const genderMap = {
male: 'M',
female: 'F'
};
const fhirGenderMap = {
M: 'male',
F: 'female'
};
const hepaticInsufficiencyMap = {
'A': { code: 'K72.90', display: 'Mild hepatic insufficiency' },
'B': { code: 'K72.91', display: 'Moderate hepatic insufficiency' },
'C': { code: 'K72.92', display: 'Severe hepatic insufficiency' }
};
const hepaticInsufficiencyReverseMap = {
'K72.90': 'A',
'K72.91': 'B',
'K72.92': 'C'
};
const calculateAgeInMonths = (birthDate) => {
const birth = new Date(birthDate);
const today = new Date();
const yearsDifference = today.getFullYear() - birth.getFullYear();
const monthsDifference = today.getMonth() - birth.getMonth();
const daysDifference = today.getDate() - birth.getDate();
let ageInMonths = yearsDifference * 12 + monthsDifference;
if (daysDifference < 0) {
ageInMonths--;
}
return ageInMonths;
};
const calculateBirthDateFromAgeInMonths = (ageInMonths) => {
const today = new Date();
const birthDate = new Date(today.setMonth(today.getMonth() - ageInMonths));
const year = birthDate.getFullYear();
const month = (birthDate.getMonth() + 1).toString().padStart(2, '0');
const day = birthDate.getDate().toString().padStart(2, '0');
return `${year}-${month}-${day}`;
};
const generatePatientId = (patientData) => {
// Create a consistent hash based on patient data
const dataToHash = {
age: patientData.age,
gender: 'sexe' in patientData ? patientData.sexe : patientData.gender,
weight: 'poids' in patientData ? patientData.poids : patientData.weight,
height: 'taille' in patientData ? patientData.taille : patientData.height,
pregnancy: 'grossesse' in patientData ? patientData.grossesse : patientData.pregnancy,
breastfeeding: 'allaitement' in patientData ? patientData.allaitement : patientData.breastfeeding
};
return generateHash(dataToHash);
};
const convertPatient = (bcbPatient) => {
return {
firstName: '',
lastName: '',
age: bcbPatient.age,
gender: bcbPatient.sexe,
weight: bcbPatient.poids,
pregnancy: bcbPatient.grossesse === 1,
breastfeeding: bcbPatient.allaitement === 1,
ammPathologies: bcbPatient.lstPathologiesAMM.map(code => ({
type: 'AMM',
code: code,
label: ''
})),
allergies: bcbPatient.lstIdComposantAllergie.map(code => ({
code: code,
label: ''
})),
hepaticStage: bcbPatient.insuffisanceHepatique,
height: bcbPatient.taille,
weeksOfPregnancy: 0,
creatinineClearance: bcbPatient.clairanceCreatinine,
molCreatinine: bcbPatient.creatininemieMol,
mglCreatinine: bcbPatient.creatininemieMg,
gfr: 0,
medicalTeam: {
firstNameDoctor: "",
lastNameDoctor: "",
firstNameSpecialist: "",
lastNameSpecialist: "",
firstNamePharmacist: "",
lastNamePharmacist: "",
firstNameNurse: "",
lastNameNurse: ""
}
};
};
const sortEntries = async (target, entries, allergiesMapper, snomedPathologiesMapper, errors = []) => {
const patientEntry = entries.find(entry => entry?.resource?.resourceType === "Patient")?.resource;
const observationEntries = entries.filter(entry => entry?.resource?.resourceType === "Observation").map(entry => entry?.resource);
const conditionEntries = entries.filter(entry => entry?.resource?.resourceType === "Condition").map(entry => entry?.resource);
const allergyIntoleranceEntries = entries.filter(entry => entry?.resource?.resourceType === "AllergyIntolerance").map(entry => entry?.resource);
const weightObservation = observationEntries.find(entry => entry?.code?.coding?.[0]?.code === '29463-7');
const heightObservation = observationEntries.find(entry => entry?.code?.coding?.[0]?.code === '8302-2');
const pregnancyObservation = observationEntries.find(entry => entry?.code?.coding?.some(coding => coding.code === '82810-3'));
const breastfeedingObservation = observationEntries.find(entry => entry?.code?.coding?.some(coding => coding.code === '63895-7'));
const hepaticCondition = conditionEntries.find(entry => entry?.code?.coding?.some(coding => coding.code === '2164-2' || coding.code === '14682-9' || coding.code === '2160-0'));
let lstIdComposantAllergie = [];
let errorField = target === 'bcb' ? 'lstIdComposantAllergie' : 'allergies';
for (const allergyEntry of allergyIntoleranceEntries) {
if (allergiesMapper) {
const mappingResult = await allergiesMapper(allergyEntry?.code?.coding?.[0]);
if (Array.isArray(mappingResult)) {
if (mappingResult.length > 0) {
lstIdComposantAllergie.push(...mappingResult);
}
else {
errors.push({
field: errorField,
message: `No mapping found for allergy code ${allergyEntry?.code?.coding?.[0]?.code}`
});
}
}
else if (mappingResult) {
lstIdComposantAllergie.push(mappingResult);
}
else {
errors.push({
field: errorField,
message: `No mapping found for allergy code ${allergyEntry?.code?.coding?.[0]?.code}`
});
}
}
else {
errors.push({
field: errorField,
message: 'Allergy Intolerance values were found but no allergies mapper was provided, the output field is thus left empty.'
});
}
}
const cim10PathologiesEntries = conditionEntries.filter(entry => entry?.code?.coding?.some(coding => coding.system === 'http://hl7.org/fhir/sid/icd-10')).map(pathology => ({ code: pathology?.code?.coding?.[0]?.code || '', label: '' }));
const snomedPathologiesEntries = conditionEntries.filter(entry => entry?.code?.coding?.some(coding => coding.system === 'http://snomed.info/sct'));
const ammPathologiesEntries = conditionEntries.filter(entry => entry?.code?.coding?.some(coding => coding.system === 'https://platform.claudebernard.fr/fhir/CodeSystem/amm-pathologies')).map(pathology => ({ code: pathology?.code?.coding?.[0]?.code || '', label: '' }));
let lstPathologies = [];
lstPathologies.push(...ammPathologiesEntries);
let lstCim10Pathologies = [];
lstCim10Pathologies.push(...cim10PathologiesEntries);
errorField = target === 'bcb' ? 'lstPathologiesAMM' : 'ammPathologies';
for (const pathology of snomedPathologiesEntries) {
if (snomedPathologiesMapper) {
const mappingResult = await snomedPathologiesMapper(pathology?.code?.coding?.[0]);
if (Array.isArray(mappingResult)) {
if (mappingResult.length > 0) {
lstPathologies.push(...mappingResult);
}
else {
errors.push({
field: errorField,
message: `No mapping found for snomed code ${pathology?.code?.coding?.[0]?.code}`
});
}
}
else if (mappingResult) {
lstPathologies.push(mappingResult);
}
else {
errors.push({
field: errorField,
message: `No mapping found for snomed code ${pathology?.code?.coding?.[0]?.code}`
});
}
}
else {
errors.push({
field: errorField,
message: "Snomed pathologies were found but no mapper was provided, the output field is thus left empty."
});
}
}
const clairanceCreatinineObservation = observationEntries.find(entry => entry?.code?.coding?.some(coding => coding.code === '2164-2'));
const creatininemieMolObservation = observationEntries.find(entry => entry?.code?.coding?.some(coding => coding.code === '14682-9'));
const creatininemieMgObservation = observationEntries.find(entry => entry?.code?.coding?.some(coding => coding.code === '2160-0'));
return {
patientEntry,
weightObservation,
heightObservation,
pregnancyObservation,
breastfeedingObservation,
hepaticCondition,
lstIdComposantAllergie,
lstCim10Pathologies,
lstPathologies,
clairanceCreatinineObservation,
creatininemieMolObservation,
creatininemieMgObservation
};
};
const patientToFhir = (patientData) => {
if (!patientData) {
return {
result: undefined,
errors: [{
field: 'root',
message: 'Invalid patient object'
}]
};
}
let errors = [];
const bundleEntries = [];
// Generate a consistent patient ID based on patient data
const patientId = generatePatientId(patientData);
const patientReference = `Patient/${patientId}`;
const patient = {
resourceType: 'Patient',
id: patientId,
birthDate: calculateBirthDateFromAgeInMonths(patientData.age),
gender: fhirGenderMap[patientData.gender] || 'unknown'
};
const weightObservation = {
resourceType: 'Observation',
id: `${patientId}-weight`,
code: {
coding: [{
code: '29463-7',
display: 'Body Weight'
}]
},
status: 'final',
valueQuantity: {
value: patientData.weight,
unit: 'kg'
},
subject: {
reference: patientReference
}
};
const heightObservation = {
resourceType: 'Observation',
id: `${patientId}-height`,
code: {
coding: [{
code: '8302-2',
display: 'Body Height'
}]
},
status: 'final',
valueQuantity: {
value: patientData.height,
unit: 'cm'
},
subject: {
reference: patientReference
}
};
let pregnancyObservation = undefined;
if (patientData.pregnancy) {
pregnancyObservation = {
resourceType: 'Observation',
id: `${patientId}-pregnancy`,
code: {
coding: [{
code: '82810-3',
display: 'Pregnancy Status'
}]
},
status: 'final',
subject: {
reference: patientReference
}
};
}
if (pregnancyObservation && patientData.weeksOfPregnancy > 0) {
pregnancyObservation.valueQuantity = {
value: patientData.weeksOfPregnancy,
unit: 'weeks',
system: 'http://unitsofmeasure.org',
code: 'wk'
};
}
let breastfeedingObservation = undefined;
if (patientData.breastfeeding) {
breastfeedingObservation = {
resourceType: 'Observation',
id: `${patientId}-breastfeeding`,
code: {
coding: [{
code: '63895-7',
display: 'Breastfeeding Status'
}]
},
status: 'final',
subject: {
reference: patientReference
}
};
}
let creatinineClearanceObservation;
if (patientData.creatinineClearance > 0) {
creatinineClearanceObservation = {
resourceType: 'Observation',
id: `${patientId}-creatinine-clearance`,
code: {
coding: [{
system: 'http://loinc.org',
code: '2164-2',
display: 'Creatinine clearance'
}]
},
status: 'final',
valueQuantity: {
value: patientData.creatinineClearance,
unit: 'mL/min'
},
subject: {
reference: patientReference
}
};
}
let creatinineMolObservation;
if (patientData.molCreatinine > 0) {
creatinineMolObservation = {
resourceType: 'Observation',
id: `${patientId}-creatinine-mol`,
code: {
coding: [{
system: 'http://loinc.org',
code: '14682-9',
display: 'Creatinine [Moles/volume] in Serum or Plasma'
}]
},
status: 'final',
valueQuantity: {
value: patientData.molCreatinine,
unit: 'mmol/L'
},
subject: {
reference: patientReference
}
};
}
let creatinineMgObservation;
if (patientData.mglCreatinine > 0) {
creatinineMgObservation = {
resourceType: 'Observation',
id: `${patientId}-creatinine-mg`,
code: {
coding: [{
system: 'http://loinc.org',
code: '2160-0',
display: 'Creatinine [Mass/volume] in Serum or Plasma'
}]
},
status: 'final',
valueQuantity: {
value: patientData.mglCreatinine,
unit: 'mg/dL'
},
subject: {
reference: patientReference
}
};
}
let conditions = [];
if (patientData.ammPathologies) {
conditions = patientData.ammPathologies.map((pathology, index) => {
return {
resourceType: 'Condition',
id: `${patientId}-condition-${index}`,
code: {
coding: [{
system: pathology.type === 'AMM' ? 'https://platform.claudebernard.fr/fhir/CodeSystem/amm-pathologies' : 'http://hl7.org/fhir/sid/icd-10',
code: pathology.code,
display: pathology.label || pathology.code
}]
},
clinicalStatus: {
coding: [{
system: 'http://terminology.hl7.org/CodeSystem/condition-clinical',
code: 'active',
display: 'Active'
}]
},
subject: {
reference: patientReference
}
};
});
}
let hepaticInsufficiencyCondition;
if (patientData.hepaticStage && hepaticInsufficiencyMap[patientData.hepaticStage]) {
const stage = hepaticInsufficiencyMap[patientData.hepaticStage];
hepaticInsufficiencyCondition = {
resourceType: 'Condition',
id: `${patientId}-hepatic-insufficiency`,
code: {
coding: [{
system: 'http://hl7.org/fhir/sid/icd-10',
code: stage.code,
display: stage.display
}]
},
clinicalStatus: {
coding: [{
system: 'http://terminology.hl7.org/CodeSystem/condition-clinical',
code: 'active',
display: 'Active'
}]
},
verificationStatus: {
coding: [{
system: 'http://terminology.hl7.org/CodeSystem/condition-ver-status',
code: 'confirmed',
display: 'Confirmed'
}]
},
subject: {
reference: patientReference
}
};
}
let allergies = [];
if (patientData.allergies) {
allergies = patientData.allergies.map((allergy, index) => {
return {
resourceType: 'AllergyIntolerance',
id: `${patientId}-allergy-${index}`,
patient: {
reference: patientReference
},
code: {
coding: [{
system: 'https://platform.claudebernard.fr/fhir/CodeSystem/products-ingredients',
code: allergy.code.toString(),
display: allergy.label?.toString() || allergy.code.toString()
}]
},
clinicalStatus: {
coding: [{
system: 'http://terminology.hl7.org/CodeSystem/condition-clinical',
code: 'active',
display: 'Active'
}]
},
};
});
}
bundleEntries.push(...[
{ resource: patient },
...(weightObservation ? [{ resource: weightObservation }] : []),
...(heightObservation ? [{ resource: heightObservation }] : []),
...(pregnancyObservation ? [{ resource: pregnancyObservation }] : []),
...(breastfeedingObservation ? [{ resource: breastfeedingObservation }] : []),
...(creatinineClearanceObservation ? [{ resource: creatinineClearanceObservation }] : []),
...(creatinineMolObservation ? [{ resource: creatinineMolObservation }] : []),
...(creatinineMgObservation ? [{ resource: creatinineMgObservation }] : []),
...conditions.map(condition => ({ resource: condition })),
...allergies.map(allergy => ({ resource: allergy })),
...(hepaticInsufficiencyCondition ? [{ resource: hepaticInsufficiencyCondition }] : []),
]);
const bundle = {
resourceType: "Bundle",
id: "patient-bundle",
type: "collection",
entry: bundleEntries
};
return {
result: bundle,
errors: errors
};
};
/* ================== FHIR <=> BCB ================== */
function bcbToFhir$1(bcbPatient) {
return patientToFhir(convertPatient(bcbPatient));
}
async function fhirToBcb$1(fhirBundle, allergiesMapper, snomedPathologiesMapper) {
if (!fhirBundle || !fhirBundle.entry || fhirBundle.entry.length === 0) {
return {
result: undefined,
errors: [{
field: 'root',
message: 'Invalid Bundle or empty entries'
}]
};
}
let errors = [];
const { patientEntry, weightObservation, heightObservation, pregnancyObservation, breastfeedingObservation, hepaticCondition, lstIdComposantAllergie, lstCim10Pathologies, lstPathologies, clairanceCreatinineObservation, creatininemieMolObservation, creatininemieMgObservation } = await sortEntries('bcb', fhirBundle.entry, wrapMaybeAsync(allergiesMapper), wrapMaybeAsync(snomedPathologiesMapper), errors);
const bcbPatient = {
lstIdComposantAllergie: lstIdComposantAllergie.map(codification => Number(codification.code)),
lstPathologiesCIM10: lstCim10Pathologies.map(codification => codification.code),
lstPathologiesAMM: lstPathologies.map(codification => codification.code),
age: patientEntry && patientEntry.birthDate ? calculateAgeInMonths(patientEntry.birthDate) : 0,
poids: weightObservation && weightObservation?.valueQuantity?.value ? weightObservation?.valueQuantity?.value : 0,
taille: heightObservation && heightObservation?.valueQuantity?.value ? heightObservation?.valueQuantity?.value : 0,
grossesse: pregnancyObservation ? 1 : 0,
allaitement: breastfeedingObservation ? 1 : 0,
sexe: patientEntry?.gender ? genderMap[patientEntry.gender] || '' : '',
clairanceCreatinine: clairanceCreatinineObservation && clairanceCreatinineObservation?.valueQuantity?.value ? clairanceCreatinineObservation?.valueQuantity?.value : 0,
creatininemieMol: creatininemieMolObservation && creatininemieMolObservation?.valueQuantity?.value ? creatininemieMolObservation?.valueQuantity?.value : 0,
creatininemieMg: creatininemieMgObservation && creatininemieMgObservation?.valueQuantity?.value ? creatininemieMgObservation?.valueQuantity?.value : 0,
insuffisanceHepatique: hepaticCondition?.code?.coding?.[0]?.code ? hepaticInsufficiencyReverseMap[hepaticCondition.code.coding[0].code] : '',
};
return {
result: bcbPatient,
errors: errors
};
}
/* ================== FHIR <=> CB ================== */
function cbToFhir(cbPatient) {
return patientToFhir(cbPatient);
}
async function fhirToCb(fhirBundle, allergiesMapper, snomedPathologiesMapper) {
if (!fhirBundle?.entry?.length) {
return {
result: undefined,
errors: [{
field: 'root',
message: 'Invalid Bundle or empty entries'
}]
};
}
let errors = [];
const { patientEntry, weightObservation, heightObservation, pregnancyObservation, breastfeedingObservation, hepaticCondition, lstIdComposantAllergie, lstCim10Pathologies, lstPathologies, clairanceCreatinineObservation, creatininemieMolObservation, creatininemieMgObservation } = await sortEntries('cb', fhirBundle.entry, wrapMaybeAsync(allergiesMapper), wrapMaybeAsync(snomedPathologiesMapper), errors);
const pathologies = [
...lstPathologies.map(codification => ({ type: 'AMM', code: codification.code, label: codification.label })),
...lstCim10Pathologies.map(codification => ({ type: 'CIM10', code: codification.code, label: codification.label }))
];
return {
result: {
firstName: '',
lastName: '',
age: patientEntry && patientEntry.birthDate ? calculateAgeInMonths(patientEntry.birthDate) : 0,
gender: patientEntry?.gender ? genderMap[patientEntry.gender] || '' : '',
weight: weightObservation && weightObservation?.valueQuantity?.value ? weightObservation?.valueQuantity?.value : 0,
pregnancy: pregnancyObservation ? true : false,
breastfeeding: breastfeedingObservation ? true : false,
ammPathologies: pathologies,
allergies: lstIdComposantAllergie.map(codification => ({ code: Number(codification.code), label: codification.label })),
hepaticStage: hepaticCondition?.code?.coding?.[0]?.code || '',
height: heightObservation && heightObservation?.valueQuantity?.value ? heightObservation?.valueQuantity?.value : 0,
weeksOfPregnancy: pregnancyObservation && pregnancyObservation?.valueQuantity?.value ? pregnancyObservation?.valueQuantity?.value : 0,
creatinineClearance: clairanceCreatinineObservation && clairanceCreatinineObservation?.valueQuantity?.value ? clairanceCreatinineObservation?.valueQuantity?.value : 0,
molCreatinine: creatininemieMolObservation && creatininemieMolObservation?.valueQuantity?.value ? creatininemieMolObservation?.valueQuantity?.value : 0,
mglCreatinine: creatininemieMgObservation && creatininemieMgObservation?.valueQuantity?.value ? creatininemieMgObservation?.valueQuantity?.value : 0,
gfr: 0,
medicalTeam: {
firstNameDoctor: "",
lastNameDoctor: "",
firstNameSpecialist: "",
lastNameSpecialist: "",
firstNamePharmacist: "",
lastNamePharmacist: "",
firstNameNurse: "",
lastNameNurse: ""
}
},
errors: errors
};
}
// Utility to wrap a possibly sync or async function so it always returns a Promise
function wrapMaybeAsync(fn) {
if (!fn)
return undefined;
return ((...args) => Promise.resolve(fn(...args)));
}
var patientMapper = /*#__PURE__*/Object.freeze({
__proto__: null,
bcbToFhir: bcbToFhir$1,
cbToFhir: cbToFhir,
fhirToBcb: fhirToBcb$1,
fhirToCb: fhirToCb
});
/**
* Generate a hash-based ID for a medication based on its codes
*/
const generateMedicationId = (codes, system) => {
const sortedCodes = [...codes].sort((a, b) => a.localeCompare(b));
const dataToHash = {
codes: sortedCodes,
system
};
return generateHash(dataToHash);
};
/**
* Helper function to extract codes from a single medication
*/
const extractCodesFromMedication = (medication, system) => {
const codes = [];
if (medication.code?.coding) {
for (const coding of medication.code.coding) {
if (coding.system === system && coding.code) {
codes.push(coding.code);
}
}
}
return codes;
};
/**
* Extracts a list of codes from a Bundle containing Medication resources based on the specified system.
* @param bundle Bundle containing Medication resources to extract codes from
* @param system The coding system to filter by
* @returns List of codes that match the specified system
*/
function extractCodesFromMedications(bundle, system) {
const codes = [];
if (!bundle.entry) {
return codes;
}
for (const entry of bundle.entry) {
if (entry.resource?.resourceType === 'Medication') {
const medication = entry.resource;
codes.push(...extractCodesFromMedication(medication, system));
}
}
return codes;
}
/**
* Extracts a list of bcb-code strings from a Bundle containing Medication resources.
* Only codes with system "https://platform.claudebernard.fr/fhir/CodeSystem/bcb-code" are included.
* @param bundle Bundle containing Medication resources
* @returns List of BCB codes
*/
function extractBcbCodesFromMedications(bundle) {
return extractCodesFromMedications(bundle, "https://platform.claudebernard.fr/fhir/CodeSystem/bcb-code");
}
/**
* Extracts a list of cip13-code strings from a Bundle containing Medi