@claudebernard/node-fhir-mapper 2.1.2 → 2.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -211,6 +211,17 @@ back by the other.
211
211
  | `lstPathologiesCIM10` | `Condition` | `http://hl7.org/fhir/sid/icd-10` |
212
212
  | `lstIdComposantAllergie` / `allergies` | `AllergyIntolerance` | `.../CodeSystem/products-ingredients` |
213
213
 
214
+ #### Codifications and mappers
215
+
216
+ Pathologies and allergies already coded in a Claude Bernard code system are read as is, their
217
+ `display` being used as the label : `amm-pathologies` and ICD-10 `Condition` resources, and
218
+ `products-ingredients` `AllergyIntolerance` resources. The optional `allergiesMapper` and
219
+ `snomedPathologiesMapper` are only needed for codings expressed in another system, SNOMED CT
220
+ typically ; an error is reported when such a coding is found and the matching mapper is missing.
221
+
222
+ The wanted coding is looked up by system, not by position, so a `Condition` or an
223
+ `AllergyIntolerance` may carry several codings without the mapping picking the wrong one.
224
+
214
225
  #### Hepatic insufficiency
215
226
 
216
227
  The Child-Pugh `Observation` carries the numeric score in `valueQuantity` and the class as a
@@ -387,7 +398,20 @@ The medication mapper provides utilities for creating and extracting medication
387
398
  function createMedicationsFromBcbCodes(codes: string[]): MappingResponse<Bundle> {}
388
399
  ```
389
400
 
390
- Creates a FHIR Bundle containing Medication resources from BCB codes.
401
+ Creates a FHIR Bundle containing Medication resources from BCB codes. Each Medication is paired with a MedicationRequest that references it. Those MedicationRequest resources are blank : they carry no `dosageInstruction`, only the mandatory `status`, `intent`, `medication` and `subject` fields.
402
+
403
+ Both resources get a UUID v4 as `id`, so their ids differ from one call to the next. Only the Bundle `id` stays deterministic, derived from the codes it contains. Each Bundle entry carries a `fullUrl` built as `<ResourceType>/<id>` :
404
+
405
+ ```json
406
+ {
407
+ "fullUrl": "Medication/d813a438-a3ea-4608-ba50-d19a69e59569",
408
+ "resource": {
409
+ "resourceType": "Medication",
410
+ "id": "d813a438-a3ea-4608-ba50-d19a69e59569",
411
+ "code": { "coding": [{ "system": "https://platform.claudebernard.fr/fhir/CodeSystem/bcb-code", "code": "BCB001" }] }
412
+ }
413
+ }
414
+ ```
391
415
 
392
416
  ##### Usage
393
417
 
@@ -398,8 +422,8 @@ const bcbCodes = ['BCB001', 'BCB002', 'BCB003'];
398
422
  const response = medicationMapper.createMedicationsFromBcbCodes(bcbCodes);
399
423
 
400
424
  if (response.result) {
401
- // Bundle with Medication resources
402
- console.log(response.result.entry?.length); // 3 medications
425
+ // Bundle with the Medication resources first, then their MedicationRequest resources
426
+ console.log(response.result.entry?.length); // 6 : 3 medications + 3 medication requests
403
427
  }
404
428
  ```
405
429
 
@@ -410,7 +434,7 @@ function createMedicationsFromCIP13Codes(codes: string[]): MappingResponse<Bundl
410
434
  function createMedicationsFromCISCodes(codes: string[]): MappingResponse<Bundle> {}
411
435
  ```
412
436
 
413
- Similar functions for creating medications from CIP13 and CIS codes respectively.
437
+ Similar functions for creating medications from CIP13 and CIS codes respectively. They also add a blank MedicationRequest per Medication.
414
438
 
415
439
  #### - extractBcbCodesFromMedications / extractCIP13CodesFromMedications / extractCISCodesFromMedications
416
440
 
package/dist/index.d.ts CHANGED
@@ -225,23 +225,26 @@ declare function extractCIP13CodesFromMedications(bundle: Bundle): string[];
225
225
  declare function extractCISCodesFromMedications(bundle: Bundle): string[];
226
226
  /**
227
227
  * Creates a Bundle containing Medication resources from a list of bcb-code strings.
228
- * Each Medication will have a CodeableConcept with a Coding for the bcb-code system.
228
+ * Each Medication will have a CodeableConcept with a Coding for the bcb-code system,
229
+ * and is paired with a MedicationRequest referencing it (without dosage instruction).
229
230
  * @param bcbCodes List of BCB codes to convert to Medication resources
230
- * @returns MappingResponse containing a Bundle with Medication resources
231
+ * @returns MappingResponse containing a Bundle with Medication and MedicationRequest resources
231
232
  */
232
233
  declare function createMedicationsFromBcbCodes(bcbCodes: string[]): MappingResponse<Bundle>;
233
234
  /**
234
235
  * Creates a Bundle containing Medication resources from a list of CIP13 codes.
235
- * Each Medication will have a CodeableConcept with a Coding for the CIP13 system.
236
+ * Each Medication will have a CodeableConcept with a Coding for the CIP13 system,
237
+ * and is paired with a MedicationRequest referencing it (without dosage instruction).
236
238
  * @param cip13Codes List of CIP13 codes to convert to Medication resources
237
- * @returns MappingResponse containing a Bundle with Medication resources
239
+ * @returns MappingResponse containing a Bundle with Medication and MedicationRequest resources
238
240
  */
239
241
  declare function createMedicationsFromCIP13Codes(cip13Codes: string[]): MappingResponse<Bundle>;
240
242
  /**
241
243
  * Creates a Bundle containing Medication resources from a list of CIS codes.
242
- * Each Medication will have a CodeableConcept with a Coding for the CIS system.
244
+ * Each Medication will have a CodeableConcept with a Coding for the CIS system,
245
+ * and is paired with a MedicationRequest referencing it (without dosage instruction).
243
246
  * @param cisCodes List of CIS codes to convert to Medication resources
244
- * @returns MappingResponse containing a Bundle with Medication resources
247
+ * @returns MappingResponse containing a Bundle with Medication and MedicationRequest resources
245
248
  */
246
249
  declare function createMedicationsFromCISCodes(cisCodes: string[]): MappingResponse<Bundle>;
247
250
 
@@ -323,5 +326,12 @@ declare const simpleHash: (str: string) => string;
323
326
  */
324
327
  declare const generateHash: (data: any) => string;
325
328
 
326
- export { dosageMapper_d as dosageMapper, generateHash, medicationMapper_d as medicationMapper, medicationRequestMapper_d as medicationRequestMapper, patientMapper_d as patientMapper, simpleHash };
329
+ /**
330
+ * UUID v4 generation compatible with both Node.js and browser environments
331
+ * Like the hash utils, this avoids importing the Node.js crypto module to keep
332
+ * the package usable in a browser bundle
333
+ */
334
+ declare const generateUuidV4: () => string;
335
+
336
+ export { dosageMapper_d as dosageMapper, generateHash, generateUuidV4, medicationMapper_d as medicationMapper, medicationRequestMapper_d as medicationRequestMapper, patientMapper_d as patientMapper, simpleHash };
327
337
  export type { CodificationFunction, Coding, MappingError, MappingResponse, SimpleCodification };
package/dist/index.js CHANGED
@@ -670,6 +670,23 @@ const LEGACY_HEPATIC_ICD10_CODES = Object.keys(hepaticInsufficiencyReverseMap);
670
670
  const isAcceptedLoincCode = (code, acceptedCodes) => !!code && acceptedCodes.includes(code);
671
671
  /** True when any coding of the concept carries one of the accepted codes. */
672
672
  const hasAcceptedCode = (concept, acceptedCodes) => !!concept?.coding?.some(coding => isAcceptedLoincCode(coding.code, acceptedCodes));
673
+ /**
674
+ * Find the coding of a concept belonging to a given system.
675
+ *
676
+ * A concept often carries several codings, so the wanted one is not necessarily the first: reading
677
+ * `coding[0]` blindly would mix up the code and the label of two different code systems.
678
+ */
679
+ const findCodingBySystem = (concept, system) => concept?.coding?.find(coding => coding.system === system);
680
+ /** Read a coding as a codification, the label being carried by its display. */
681
+ const codingToCodification = (coding) => ({
682
+ code: coding.code ?? '',
683
+ label: coding.display ?? ''
684
+ });
685
+ /** Codifications of every condition coded in the given system, labels included. */
686
+ const conditionsToCodifications = (conditions, system) => conditions.flatMap(condition => {
687
+ const coding = findCodingBySystem(condition?.code, system);
688
+ return coding ? [codingToCodification(coding)] : [];
689
+ });
673
690
  /**
674
691
  * Body Surface Area, Du Bois formula.
675
692
  *
@@ -789,6 +806,21 @@ const sortEntries = async (target, entries, allergiesMapper, snomedPathologiesMa
789
806
  let lstIdComposantAllergie = [];
790
807
  let errorField = target === 'bcb' ? 'lstIdComposantAllergie' : 'allergies';
791
808
  for (const allergyEntry of allergyIntoleranceEntries) {
809
+ // An allergy already coded in the BCB ingredients system needs no mapper, its code is the
810
+ // idComposant expected on the BCB / CB side. This mirrors how AMM conditions are read.
811
+ const ingredientCoding = findCodingBySystem(allergyEntry?.code, INGREDIENTS_SYSTEM_URL);
812
+ if (ingredientCoding) {
813
+ if (ingredientCoding.code && !Number.isNaN(Number(ingredientCoding.code))) {
814
+ lstIdComposantAllergie.push(codingToCodification(ingredientCoding));
815
+ }
816
+ else {
817
+ errors.push({
818
+ field: errorField,
819
+ message: `Invalid ingredient code for allergy ${ingredientCoding.code ?? ''}`
820
+ });
821
+ }
822
+ continue;
823
+ }
792
824
  if (allergiesMapper) {
793
825
  const mappingResult = await allergiesMapper(allergyEntry?.code?.coding?.[0]);
794
826
  if (Array.isArray(mappingResult)) {
@@ -821,17 +853,18 @@ const sortEntries = async (target, entries, allergiesMapper, snomedPathologiesMa
821
853
  }
822
854
  // The legacy hepatic Condition is already consumed as the hepatic status, keep it out of the
823
855
  // ICD-10 pathologies so it is not reported twice.
824
- const cim10PathologiesEntries = conditionEntries.filter(entry => entry !== legacyHepaticCondition && entry?.code?.coding?.some(coding => coding.system === ICD10_SYSTEM_URL)).map(pathology => ({ code: pathology?.code?.coding?.[0]?.code || '', label: '' }));
856
+ const cim10PathologiesEntries = conditionsToCodifications(conditionEntries.filter(entry => entry !== legacyHepaticCondition), ICD10_SYSTEM_URL);
825
857
  const snomedPathologiesEntries = conditionEntries.filter(entry => entry?.code?.coding?.some(coding => coding.system === SNOMED_SYSTEM_URL));
826
- const ammPathologiesEntries = conditionEntries.filter(entry => entry?.code?.coding?.some(coding => coding.system === AMM_SYSTEM_URL)).map(pathology => ({ code: pathology?.code?.coding?.[0]?.code || '', label: '' }));
858
+ const ammPathologiesEntries = conditionsToCodifications(conditionEntries, AMM_SYSTEM_URL);
827
859
  let lstPathologies = [];
828
860
  lstPathologies.push(...ammPathologiesEntries);
829
861
  let lstCim10Pathologies = [];
830
862
  lstCim10Pathologies.push(...cim10PathologiesEntries);
831
863
  errorField = target === 'bcb' ? 'lstPathologiesAMM' : 'ammPathologies';
832
864
  for (const pathology of snomedPathologiesEntries) {
865
+ const snomedCoding = findCodingBySystem(pathology?.code, SNOMED_SYSTEM_URL);
833
866
  if (snomedPathologiesMapper) {
834
- const mappingResult = await snomedPathologiesMapper(pathology?.code?.coding?.[0]);
867
+ const mappingResult = await snomedPathologiesMapper(snomedCoding);
835
868
  if (Array.isArray(mappingResult)) {
836
869
  if (mappingResult.length > 0) {
837
870
  lstPathologies.push(...mappingResult);
@@ -839,7 +872,7 @@ const sortEntries = async (target, entries, allergiesMapper, snomedPathologiesMa
839
872
  else {
840
873
  errors.push({
841
874
  field: errorField,
842
- message: `No mapping found for snomed code ${pathology?.code?.coding?.[0]?.code}`
875
+ message: `No mapping found for snomed code ${snomedCoding?.code}`
843
876
  });
844
877
  }
845
878
  }
@@ -849,7 +882,7 @@ const sortEntries = async (target, entries, allergiesMapper, snomedPathologiesMa
849
882
  else {
850
883
  errors.push({
851
884
  field: errorField,
852
- message: `No mapping found for snomed code ${pathology?.code?.coding?.[0]?.code}`
885
+ message: `No mapping found for snomed code ${snomedCoding?.code}`
853
886
  });
854
887
  }
855
888
  }
@@ -1373,6 +1406,39 @@ var patientMapper = /*#__PURE__*/Object.freeze({
1373
1406
  fhirToCb: fhirToCb
1374
1407
  });
1375
1408
 
1409
+ /**
1410
+ * UUID v4 generation compatible with both Node.js and browser environments
1411
+ * Like the hash utils, this avoids importing the Node.js crypto module to keep
1412
+ * the package usable in a browser bundle
1413
+ */
1414
+ const generateUuidV4 = () => {
1415
+ const webCrypto = globalThis.crypto;
1416
+ if (typeof webCrypto?.randomUUID === 'function') {
1417
+ return webCrypto.randomUUID();
1418
+ }
1419
+ const bytes = new Uint8Array(16);
1420
+ if (typeof webCrypto?.getRandomValues === 'function') {
1421
+ // Available in browsers even outside of a secure context, where randomUUID is not
1422
+ webCrypto.getRandomValues(bytes);
1423
+ }
1424
+ else {
1425
+ for (let i = 0; i < bytes.length; i++) {
1426
+ bytes[i] = Math.floor(Math.random() * 256);
1427
+ }
1428
+ }
1429
+ // Force the version (4) and variant (10xx) bits required by RFC 4122
1430
+ bytes[6] = (bytes[6] & 0x0f) | 0x40;
1431
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
1432
+ const hex = Array.from(bytes, byte => byte.toString(16).padStart(2, '0')).join('');
1433
+ return [
1434
+ hex.substring(0, 8),
1435
+ hex.substring(8, 12),
1436
+ hex.substring(12, 16),
1437
+ hex.substring(16, 20),
1438
+ hex.substring(20)
1439
+ ].join('-');
1440
+ };
1441
+
1376
1442
  /**
1377
1443
  * Generate a hash-based ID for a medication based on its codes
1378
1444
  */
@@ -1445,13 +1511,24 @@ function extractCISCodesFromMedications(bundle) {
1445
1511
  return extractCodesFromMedications(bundle, "http://terminology.hl7.org/CodeSystem/cis-code");
1446
1512
  }
1447
1513
  /**
1448
- * Creates a Bundle containing Medication resources from a list of bcb-code strings.
1449
- * Each Medication will have a CodeableConcept with a Coding for the bcb-code system.
1450
- * @param bcbCodes List of BCB codes to convert to Medication resources
1451
- * @returns MappingResponse containing a Bundle with Medication resources
1514
+ * Wraps a resource into a Bundle entry, carrying its fullUrl as "<ResourceType>/<id>"
1452
1515
  */
1453
- function createMedicationsFromBcbCodes(bcbCodes) {
1454
- if (!bcbCodes || bcbCodes.length === 0) {
1516
+ const toBundleEntry = (resource) => ({
1517
+ fullUrl: `${resource.resourceType}/${resource.id}`,
1518
+ resource: resource
1519
+ });
1520
+ /**
1521
+ * Creates a Bundle containing a Medication and its associated MedicationRequest for each code.
1522
+ * Each Medication holds a CodeableConcept with a Coding for the given system, and each
1523
+ * MedicationRequest references its Medication without any dosage instruction.
1524
+ * Resource ids are UUID v4, so they differ from one call to the next, and every entry
1525
+ * carries a fullUrl built as "<ResourceType>/<id>".
1526
+ * @param codes List of codes to convert to Medication / MedicationRequest resources
1527
+ * @param system The coding system the codes belong to
1528
+ * @returns MappingResponse containing a Bundle with Medication and MedicationRequest resources
1529
+ */
1530
+ const createMedicationsFromCodes = (codes, system) => {
1531
+ if (!codes || codes.length === 0) {
1455
1532
  return {
1456
1533
  result: {
1457
1534
  resourceType: "Bundle",
@@ -1462,116 +1539,74 @@ function createMedicationsFromBcbCodes(bcbCodes) {
1462
1539
  errors: []
1463
1540
  };
1464
1541
  }
1465
- const bundleId = generateMedicationId(bcbCodes, "https://platform.claudebernard.fr/fhir/CodeSystem/bcb-code");
1466
- const medications = bcbCodes.map((code, index) => {
1467
- const medicationId = `${bundleId}-med-${index}`;
1468
- return {
1469
- resourceType: 'Medication',
1470
- id: medicationId,
1471
- code: {
1472
- coding: [{
1473
- system: "https://platform.claudebernard.fr/fhir/CodeSystem/bcb-code",
1474
- code: code
1475
- }]
1542
+ const bundleId = generateMedicationId(codes, system);
1543
+ const medications = codes.map(code => ({
1544
+ resourceType: 'Medication',
1545
+ id: generateUuidV4(),
1546
+ code: {
1547
+ coding: [{
1548
+ system: system,
1549
+ code: code
1550
+ }]
1551
+ }
1552
+ }));
1553
+ const medicationRequests = medications.map(medication => ({
1554
+ resourceType: 'MedicationRequest',
1555
+ id: generateUuidV4(),
1556
+ status: 'active',
1557
+ intent: 'order',
1558
+ medication: {
1559
+ reference: {
1560
+ reference: `Medication/${medication.id}`
1476
1561
  }
1477
- };
1478
- });
1562
+ },
1563
+ subject: {
1564
+ reference: 'Patient/unknown'
1565
+ }
1566
+ }));
1479
1567
  const bundle = {
1480
1568
  resourceType: "Bundle",
1481
1569
  id: `medications-bundle-${bundleId}`,
1482
1570
  type: "collection",
1483
- entry: medications.map(medication => ({ resource: medication }))
1571
+ entry: [
1572
+ ...medications.map(medication => toBundleEntry(medication)),
1573
+ ...medicationRequests.map(medicationRequest => toBundleEntry(medicationRequest))
1574
+ ]
1484
1575
  };
1485
1576
  return {
1486
1577
  result: bundle,
1487
1578
  errors: []
1488
1579
  };
1580
+ };
1581
+ /**
1582
+ * Creates a Bundle containing Medication resources from a list of bcb-code strings.
1583
+ * Each Medication will have a CodeableConcept with a Coding for the bcb-code system,
1584
+ * and is paired with a MedicationRequest referencing it (without dosage instruction).
1585
+ * @param bcbCodes List of BCB codes to convert to Medication resources
1586
+ * @returns MappingResponse containing a Bundle with Medication and MedicationRequest resources
1587
+ */
1588
+ function createMedicationsFromBcbCodes(bcbCodes) {
1589
+ return createMedicationsFromCodes(bcbCodes, "https://platform.claudebernard.fr/fhir/CodeSystem/bcb-code");
1489
1590
  }
1490
1591
  /**
1491
1592
  * Creates a Bundle containing Medication resources from a list of CIP13 codes.
1492
- * Each Medication will have a CodeableConcept with a Coding for the CIP13 system.
1593
+ * Each Medication will have a CodeableConcept with a Coding for the CIP13 system,
1594
+ * and is paired with a MedicationRequest referencing it (without dosage instruction).
1493
1595
  * @param cip13Codes List of CIP13 codes to convert to Medication resources
1494
- * @returns MappingResponse containing a Bundle with Medication resources
1596
+ * @returns MappingResponse containing a Bundle with Medication and MedicationRequest resources
1495
1597
  */
1496
1598
  function createMedicationsFromCIP13Codes(cip13Codes) {
1497
- if (!cip13Codes || cip13Codes.length === 0) {
1498
- return {
1499
- result: {
1500
- resourceType: "Bundle",
1501
- id: "empty-medications-bundle",
1502
- type: "collection",
1503
- entry: []
1504
- },
1505
- errors: []
1506
- };
1507
- }
1508
- const bundleId = generateMedicationId(cip13Codes, "http://terminology.hl7.org/CodeSystem/cip13-code");
1509
- const medications = cip13Codes.map((code, index) => {
1510
- const medicationId = `${bundleId}-med-${index}`;
1511
- return {
1512
- resourceType: 'Medication',
1513
- id: medicationId,
1514
- code: {
1515
- coding: [{
1516
- system: "http://terminology.hl7.org/CodeSystem/cip13-code",
1517
- code: code
1518
- }]
1519
- }
1520
- };
1521
- });
1522
- const bundle = {
1523
- resourceType: "Bundle",
1524
- id: `medications-bundle-${bundleId}`,
1525
- type: "collection",
1526
- entry: medications.map(medication => ({ resource: medication }))
1527
- };
1528
- return {
1529
- result: bundle,
1530
- errors: []
1531
- };
1599
+ return createMedicationsFromCodes(cip13Codes, "http://terminology.hl7.org/CodeSystem/cip13-code");
1532
1600
  }
1533
1601
  /**
1534
1602
  * Creates a Bundle containing Medication resources from a list of CIS codes.
1535
- * Each Medication will have a CodeableConcept with a Coding for the CIS system.
1603
+ * Each Medication will have a CodeableConcept with a Coding for the CIS system,
1604
+ * and is paired with a MedicationRequest referencing it (without dosage instruction).
1536
1605
  * @param cisCodes List of CIS codes to convert to Medication resources
1537
- * @returns MappingResponse containing a Bundle with Medication resources
1606
+ * @returns MappingResponse containing a Bundle with Medication and MedicationRequest resources
1538
1607
  */
1539
1608
  function createMedicationsFromCISCodes(cisCodes) {
1540
- if (!cisCodes || cisCodes.length === 0) {
1541
- return {
1542
- result: {
1543
- resourceType: "Bundle",
1544
- id: "empty-medications-bundle",
1545
- type: "collection",
1546
- entry: []
1547
- },
1548
- errors: []
1549
- };
1550
- }
1551
- const bundleId = generateMedicationId(cisCodes, "http://terminology.hl7.org/CodeSystem/cis-code");
1552
- const medications = cisCodes.map((code, index) => {
1553
- const medicationId = `${bundleId}-med-${index}`;
1554
- return {
1555
- resourceType: 'Medication',
1556
- id: medicationId,
1557
- code: {
1558
- coding: [{
1559
- system: "http://terminology.hl7.org/CodeSystem/cis-code",
1560
- code: code
1561
- }]
1562
- }
1563
- };
1564
- });
1565
- const bundle = {
1566
- resourceType: "Bundle",
1567
- id: `medications-bundle-${bundleId}`,
1568
- type: "collection",
1569
- entry: medications.map(medication => ({ resource: medication }))
1570
- };
1571
- return {
1572
- result: bundle,
1573
- errors: []
1574
- };
1609
+ return createMedicationsFromCodes(cisCodes, "http://terminology.hl7.org/CodeSystem/cis-code");
1575
1610
  }
1576
1611
 
1577
1612
  var medicationMapper = /*#__PURE__*/Object.freeze({
@@ -1747,5 +1782,5 @@ var medicationRequestMapper = /*#__PURE__*/Object.freeze({
1747
1782
  generateMedicationRequestId: generateMedicationRequestId
1748
1783
  });
1749
1784
 
1750
- export { dosageMapper, generateHash, medicationMapper, medicationRequestMapper, patientMapper, simpleHash };
1785
+ export { dosageMapper, generateHash, generateUuidV4, medicationMapper, medicationRequestMapper, patientMapper, simpleHash };
1751
1786
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claudebernard/node-fhir-mapper",
3
- "version": "2.1.2",
3
+ "version": "2.1.3",
4
4
  "description": "A simple FHIR / BCB resource mapper to help stay interoperable while still using the Claude Bernard intelligence",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",