@precisa-saude/fhir 0.20.0 → 0.20.1

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.
@@ -0,0 +1,9 @@
1
+ // src/code-systems.ts
2
+ var LOINC_SYSTEM = "http://loinc.org";
3
+ var BIOMARKER_CODE_SYSTEM = "http://fhir-brasil.dev/biomarker-codes";
4
+
5
+ export {
6
+ LOINC_SYSTEM,
7
+ BIOMARKER_CODE_SYSTEM
8
+ };
9
+ //# sourceMappingURL=chunk-A6HR4XDK.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/code-systems.ts"],"sourcesContent":["/**\n * Identificadores de sistema de código usados nos recursos FHIR.\n *\n * Ficam num módulo próprio porque exportação e importação precisam concordar\n * literalmente: o importador procura o coding pelo `system`, e uma string\n * divergente de um lado faz o biomarcador desaparecer sem erro.\n */\n\n/** LOINC, o vocabulário oficial de exames laboratoriais. */\nexport const LOINC_SYSTEM = 'http://loinc.org';\n\n/**\n * Códigos internos do fhir-brasil.\n *\n * Cobre o que não tem LOINC publicado: composição corporal por DEXA,\n * densidade óssea e escore de cálcio, entre outros. É o que permite o ciclo\n * exportar/importar preservar esses biomarcadores.\n */\nexport const BIOMARKER_CODE_SYSTEM = 'http://fhir-brasil.dev/biomarker-codes';\n"],"mappings":";AASO,IAAM,eAAe;AASrB,IAAM,wBAAwB;","names":[]}
@@ -0,0 +1,165 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }
2
+
3
+
4
+ var _chunkOR67NJDZcjs = require('./chunk-OR67NJDZ.cjs');
5
+
6
+
7
+
8
+
9
+
10
+
11
+ var _chunkNXOTXKVBcjs = require('./chunk-NXOTXKVB.cjs');
12
+
13
+
14
+ var _chunk3ILBFLVQcjs = require('./chunk-3ILBFLVQ.cjs');
15
+
16
+ // src/importer.ts
17
+ var MAX_OBSERVATIONS = 5e3;
18
+ var MAX_FILE_SIZE = 15 * 1024 * 1024;
19
+ function resolveBiomarkerCode(observation) {
20
+ const coding = _nullishCoalesce(_optionalChain([observation, 'access', _ => _.code, 'optionalAccess', _2 => _2.coding]), () => ( []));
21
+ const loincCode = _optionalChain([coding, 'access', _3 => _3.find, 'call', _4 => _4((c) => c.system === _chunkOR67NJDZcjs.LOINC_SYSTEM), 'optionalAccess', _5 => _5.code]);
22
+ const declaredCode = _optionalChain([coding, 'access', _6 => _6.find, 'call', _7 => _7((c) => c.system === _chunkOR67NJDZcjs.BIOMARKER_CODE_SYSTEM), 'optionalAccess', _8 => _8.code]);
23
+ const seenCodes = [
24
+ ...loincCode ? [`LOINC ${loincCode}`] : [],
25
+ ...declaredCode ? [`biomarker code ${declaredCode}`] : []
26
+ ];
27
+ let reason;
28
+ if (seenCodes.length > 0) {
29
+ reason = `Unknown code: ${seenCodes.join(", ")}`;
30
+ } else if (coding.length > 0) {
31
+ const systems = [...new Set(coding.map((c) => _nullishCoalesce(c.system, () => ( "(sem system)"))))];
32
+ reason = `No code in a supported system (found: ${systems.join(", ")})`;
33
+ } else {
34
+ reason = "No code found in observation coding";
35
+ }
36
+ const fromLoinc = loincCode ? _chunkNXOTXKVBcjs.loincToCode.call(void 0, loincCode) : void 0;
37
+ if (fromLoinc) return { internalCode: fromLoinc, loincCode, reason };
38
+ if (declaredCode && _chunkNXOTXKVBcjs.isValidCode.call(void 0, declaredCode)) {
39
+ const canonical = _chunkNXOTXKVBcjs.normalizeCode.call(void 0, declaredCode);
40
+ return { internalCode: canonical, loincCode: _chunkNXOTXKVBcjs.codeToLoinc.call(void 0, canonical), reason };
41
+ }
42
+ return { loincCode, reason };
43
+ }
44
+ function extractFlag(observation) {
45
+ const code = _optionalChain([observation, 'access', _9 => _9.interpretation, 'optionalAccess', _10 => _10[0], 'optionalAccess', _11 => _11.coding, 'optionalAccess', _12 => _12[0], 'optionalAccess', _13 => _13.code]);
46
+ if (code === "H" || code === "HH") return "H";
47
+ if (code === "L" || code === "LL") return "L";
48
+ return "";
49
+ }
50
+ function extractObservationsFromBundle(bundle) {
51
+ const observations = [];
52
+ const skipped = [];
53
+ for (let i = 0; i < bundle.entry.length; i++) {
54
+ const entry = bundle.entry[i];
55
+ if (!entry.resource) {
56
+ skipped.push({ index: i, reason: "Entry has no resource" });
57
+ continue;
58
+ }
59
+ if (entry.resource.resourceType !== "Observation") {
60
+ continue;
61
+ }
62
+ if (observations.length >= MAX_OBSERVATIONS) {
63
+ skipped.push({ index: i, reason: `Maximum of ${MAX_OBSERVATIONS} observations exceeded` });
64
+ continue;
65
+ }
66
+ observations.push(entry.resource);
67
+ }
68
+ return { observations, skipped };
69
+ }
70
+ function mapFHIRObservationToInternal(observation, index) {
71
+ const { internalCode, loincCode, reason } = resolveBiomarkerCode(observation);
72
+ if (!internalCode) {
73
+ return {
74
+ skipped: { index, loincCode, reason, resourceType: "Observation" }
75
+ };
76
+ }
77
+ const definition = _chunkNXOTXKVBcjs.getDefinitionByCode.call(void 0, internalCode);
78
+ let value;
79
+ let unit = "";
80
+ let isQualitative = false;
81
+ if (_optionalChain([observation, 'access', _14 => _14.valueQuantity, 'optionalAccess', _15 => _15.value]) !== void 0) {
82
+ value = observation.valueQuantity.value;
83
+ unit = observation.valueQuantity.unit || observation.valueQuantity.code || "";
84
+ } else if (observation.valueString) {
85
+ value = observation.valueString;
86
+ isQualitative = true;
87
+ } else {
88
+ return {
89
+ skipped: {
90
+ index,
91
+ loincCode,
92
+ reason: "Observation has no value (valueQuantity or valueString)",
93
+ resourceType: "Observation"
94
+ }
95
+ };
96
+ }
97
+ const collectionDate = observation.effectiveDateTime || _optionalChain([observation, 'access', _16 => _16.effectivePeriod, 'optionalAccess', _17 => _17.start]) || "";
98
+ if (!collectionDate) {
99
+ return {
100
+ skipped: {
101
+ index,
102
+ loincCode,
103
+ reason: "Observation has no effectiveDateTime or effectivePeriod.start",
104
+ resourceType: "Observation"
105
+ }
106
+ };
107
+ }
108
+ let referenceMin;
109
+ let referenceMax;
110
+ if (_optionalChain([observation, 'access', _18 => _18.referenceRange, 'optionalAccess', _19 => _19[0]])) {
111
+ referenceMin = _optionalChain([observation, 'access', _20 => _20.referenceRange, 'access', _21 => _21[0], 'access', _22 => _22.low, 'optionalAccess', _23 => _23.value]);
112
+ referenceMax = _optionalChain([observation, 'access', _24 => _24.referenceRange, 'access', _25 => _25[0], 'access', _26 => _26.high, 'optionalAccess', _27 => _27.value]);
113
+ }
114
+ const imported = {
115
+ biomarkerCode: internalCode,
116
+ biomarkerName: _optionalChain([definition, 'optionalAccess', _28 => _28.names, 'access', _29 => _29.pt, 'access', _30 => _30[0]]) || _optionalChain([definition, 'optionalAccess', _31 => _31.names, 'access', _32 => _32.en, 'access', _33 => _33[0]]) || observation.code.text || internalCode,
117
+ collectionDate,
118
+ flag: extractFlag(observation),
119
+ isQualitative,
120
+ loincCode,
121
+ referenceMax,
122
+ referenceMin,
123
+ unit: unit || _optionalChain([definition, 'optionalAccess', _34 => _34.unit]) || "",
124
+ value
125
+ };
126
+ return { observation: imported };
127
+ }
128
+ function processImportBundle(data) {
129
+ const validationErrors = _chunk3ILBFLVQcjs.validateFHIRImportBundle.call(void 0, data);
130
+ if (validationErrors.length > 0) {
131
+ return {
132
+ errors: validationErrors,
133
+ imported: [],
134
+ skipped: [],
135
+ totalProcessed: 0
136
+ };
137
+ }
138
+ const bundle = data;
139
+ const { observations, skipped } = extractObservationsFromBundle(bundle);
140
+ const imported = [];
141
+ const allSkipped = [...skipped];
142
+ for (let i = 0; i < observations.length; i++) {
143
+ const result = mapFHIRObservationToInternal(observations[i], i);
144
+ if ("observation" in result) {
145
+ imported.push(result.observation);
146
+ } else {
147
+ allSkipped.push(result.skipped);
148
+ }
149
+ }
150
+ return {
151
+ errors: [],
152
+ imported,
153
+ skipped: allSkipped,
154
+ totalProcessed: observations.length
155
+ };
156
+ }
157
+
158
+
159
+
160
+
161
+
162
+
163
+
164
+ exports.MAX_OBSERVATIONS = MAX_OBSERVATIONS; exports.MAX_FILE_SIZE = MAX_FILE_SIZE; exports.extractObservationsFromBundle = extractObservationsFromBundle; exports.mapFHIRObservationToInternal = mapFHIRObservationToInternal; exports.processImportBundle = processImportBundle;
165
+ //# sourceMappingURL=chunk-ETZXOGAB.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/home/runner/work/fhir-brasil/fhir-brasil/packages/core/dist/chunk-ETZXOGAB.cjs","../src/importer.ts"],"names":[],"mappings":"AAAA;AACE;AACA;AACF,wDAA6B;AAC7B;AACE;AACA;AACA;AACA;AACA;AACF,wDAA6B;AAC7B;AACE;AACF,wDAA6B;AAC7B;AACA;ACgDA,IAAM,iBAAA,EAAmB,GAAA;AACzB,IAAM,cAAA,EAAgB,GAAA,EAAK,KAAA,EAAO,IAAA;AAclC,SAAS,oBAAA,CAAqB,WAAA,EAI5B;AACA,EAAA,MAAM,OAAA,mCAAS,WAAA,mBAAY,IAAA,6BAAM,QAAA,UAAU,CAAC,GAAA;AAC5C,EAAA,MAAM,UAAA,kBAAY,MAAA,qBAAO,IAAA,mBAAK,CAAC,CAAA,EAAA,GAAM,CAAA,CAAE,OAAA,IAAW,8BAAY,CAAA,6BAAG,MAAA;AACjE,EAAA,MAAM,aAAA,kBAAe,MAAA,qBAAO,IAAA,mBAAK,CAAC,CAAA,EAAA,GAAM,CAAA,CAAE,OAAA,IAAW,uCAAqB,CAAA,6BAAG,MAAA;AAE7E,EAAA,MAAM,UAAA,EAAY;AAAA,IAChB,GAAI,UAAA,EAAY,CAAC,CAAA,MAAA,EAAS,SAAS,CAAA,CAAA;AACf,IAAA;AACtB,EAAA;AAOI,EAAA;AACsB,EAAA;AACE,IAAA;AACE,EAAA;AACO,IAAA;AAC1B,IAAA;AACJ,EAAA;AACI,IAAA;AACX,EAAA;AAE8B,EAAA;AACN,EAAA;AAEQ,EAAA;AAOE,IAAA;AAIE,IAAA;AACpC,EAAA;AAE2B,EAAA;AAC7B;AAKmE;AACxC,EAAA;AACU,EAAA;AACA,EAAA;AAC5B,EAAA;AACT;AAK8C;AAIH,EAAA;AACR,EAAA;AAEA,EAAA;AACH,IAAA;AACP,IAAA;AACc,MAAA;AACjC,MAAA;AACF,IAAA;AAEmB,IAAA;AAEjB,MAAA;AACF,IAAA;AAE2B,IAAA;AACQ,MAAA;AACjC,MAAA;AACF,IAAA;AAEmD,IAAA;AACrD,EAAA;AAE+B,EAAA;AACjC;AAME;AAGiC,EAAA;AAEd,EAAA;AACV,IAAA;AACwB,MAAA;AAC/B,IAAA;AACF,EAAA;AAEmB,EAAA;AAGf,EAAA;AACO,EAAA;AACS,EAAA;AAEW,EAAA;AACK,IAAA;AACD,IAAA;AACC,EAAA;AACd,IAAA;AACJ,IAAA;AACX,EAAA;AACE,IAAA;AACI,MAAA;AACP,QAAA;AACA,QAAA;AACQ,QAAA;AACM,QAAA;AAChB,MAAA;AACF,IAAA;AACF,EAAA;AAGmC,EAAA;AACd,EAAA;AACZ,IAAA;AACI,MAAA;AACP,QAAA;AACA,QAAA;AACQ,QAAA;AACM,QAAA;AAChB,MAAA;AACF,IAAA;AACF,EAAA;AAGI,EAAA;AACA,EAAA;AACiC,EAAA;AACR,IAAA;AACA,IAAA;AAC7B,EAAA;AAEsC,EAAA;AACrB,IAAA;AAEK,IAAA;AACpB,IAAA;AAC6B,IAAA;AAC7B,IAAA;AACA,IAAA;AACA,IAAA;AACA,IAAA;AACkC,IAAA;AAClC,IAAA;AACF,EAAA;AAE+B,EAAA;AACjC;AAKqE;AAE1C,EAAA;AACQ,EAAA;AACxB,IAAA;AACG,MAAA;AACG,MAAA;AACD,MAAA;AACM,MAAA;AAClB,IAAA;AACF,EAAA;AAEe,EAAA;AAGmB,EAAA;AAGO,EAAA;AACK,EAAA;AAEb,EAAA;AAChB,IAAA;AAEc,IAAA;AACK,MAAA;AAC3B,IAAA;AACyB,MAAA;AAChC,IAAA;AACF,EAAA;AAEO,EAAA;AACI,IAAA;AACT,IAAA;AACS,IAAA;AACoB,IAAA;AAC/B,EAAA;AACF;ADnIuC;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"/home/runner/work/fhir-brasil/fhir-brasil/packages/core/dist/chunk-ETZXOGAB.cjs","sourcesContent":[null,"/**\n * FHIR Importer\n *\n * Parses FHIR R4 Bundles and extracts Observation resources with known LOINC codes,\n * mapping them to internal biomarker codes for storage as lab results.\n */\n\nimport {\n codeToLoinc,\n getDefinitionByCode,\n isValidCode,\n loincToCode,\n normalizeCode,\n} from './biomarkers';\nimport { BIOMARKER_CODE_SYSTEM, LOINC_SYSTEM } from './code-systems';\nimport type { FHIRBundle, FHIRObservation } from './fhir-types';\nimport { validateFHIRImportBundle } from './validators';\n\nexport interface ImportedObservation {\n biomarkerCode: string;\n biomarkerName: string;\n collectionDate: string;\n flag: 'H' | 'L' | '';\n isQualitative: boolean;\n /** Ausente nos biomarcadores sem LOINC publicado, como composição corporal. */\n loincCode?: string;\n referenceMax?: number;\n referenceMin?: number;\n unit: string;\n value: number | string;\n}\n\nexport interface SkippedEntry {\n index: number;\n loincCode?: string;\n reason: string;\n resourceType?: string;\n}\n\nexport interface ImportError {\n details: string;\n field: string;\n}\n\nexport interface FHIRImportResult {\n errors: ImportError[];\n imported: ImportedObservation[];\n skipped: SkippedEntry[];\n totalProcessed: number;\n}\n\n/**\n * Limites de importação.\n *\n * Uma Observation exportada ocupa cerca de 1,25KB em JSON compacto e 2,75KB\n * quando o arquivo vem indentado, medido sobre um histórico real de 998\n * Observations em 61 laudos. Nesse tamanho, 5000 Observations dão 6,0MB\n * compactos ou 13,1MB indentados, e por isso o teto de arquivo é 15MB: cobre\n * as duas formas com folga.\n *\n * Na densidade desse mesmo histórico (16 Observations por laudo), 5000\n * equivalem a cerca de 300 laudos.\n */\nconst MAX_OBSERVATIONS = 5000;\nconst MAX_FILE_SIZE = 15 * 1024 * 1024; // 15MB\n\n/**\n * Resolve o código interno do biomarcador a partir do `code.coding`.\n *\n * LOINC primeiro, que é o vocabulário que arquivos de terceiros usam. Quando\n * não resolve, cai para o coding de códigos internos, presente nos arquivos\n * exportados pela própria plataforma.\n *\n * O fallback cobre dois casos: biomarcadores sem LOINC publicado (composição\n * corporal, densidade óssea, escore de cálcio) e arquivos antigos, exportados\n * quando esses biomarcadores saíam com o placeholder `99999-9`, que não\n * resolve para nada.\n */\nfunction resolveBiomarkerCode(observation: FHIRObservation): {\n internalCode?: string;\n loincCode?: string;\n reason: string;\n} {\n const coding = observation.code?.coding ?? [];\n const loincCode = coding.find((c) => c.system === LOINC_SYSTEM)?.code;\n const declaredCode = coding.find((c) => c.system === BIOMARKER_CODE_SYSTEM)?.code;\n\n const seenCodes = [\n ...(loincCode ? [`LOINC ${loincCode}`] : []),\n ...(declaredCode ? [`biomarker code ${declaredCode}`] : []),\n ];\n\n // Três motivos distintos de descarte, que antes se confundiam num só. Sem\n // essa separação, um arquivo em SNOMED relatava \"nenhum código encontrado\",\n // sugerindo `coding` vazio quando na verdade o código existia e estava num\n // system que não tratamos. Para importação de terceiros, é a diferença entre\n // um diagnóstico acionável e um enigma.\n let reason: string;\n if (seenCodes.length > 0) {\n reason = `Unknown code: ${seenCodes.join(', ')}`;\n } else if (coding.length > 0) {\n const systems = [...new Set(coding.map((c) => c.system ?? '(sem system)'))];\n reason = `No code in a supported system (found: ${systems.join(', ')})`;\n } else {\n reason = 'No code found in observation coding';\n }\n\n const fromLoinc = loincCode ? loincToCode(loincCode) : undefined;\n if (fromLoinc) return { internalCode: fromLoinc, loincCode, reason };\n\n if (declaredCode && isValidCode(declaredCode)) {\n // `isValidCode` aceita alias, mas alias não serve como código armazenado:\n // 49 definições têm um, e tanto `codeToLoinc` quanto as faixas de\n // referência são indexadas só pelo canônico. Sem normalizar, `VLDL_Cholesterol`\n // entraria no lugar de `VLDL` e perderia o LOINC 13458-5 que ele tem.\n // Importar um Bundle é fronteira de dados, que é onde `normalizeCode` deve\n // ser aplicado.\n const canonical = normalizeCode(declaredCode);\n\n // `codeToLoinc` devolve undefined para quem não tem LOINC, que é o caso\n // esperado aqui. O campo fica de fora em vez de receber um valor inventado.\n return { internalCode: canonical, loincCode: codeToLoinc(canonical), reason };\n }\n\n return { loincCode, reason };\n}\n\n/**\n * Extract interpretation flag from Observation\n */\nfunction extractFlag(observation: FHIRObservation): 'H' | 'L' | '' {\n const code = observation.interpretation?.[0]?.coding?.[0]?.code;\n if (code === 'H' || code === 'HH') return 'H';\n if (code === 'L' || code === 'LL') return 'L';\n return '';\n}\n\n/**\n * Extract Observation resources from a FHIR Bundle\n */\nexport function extractObservationsFromBundle(bundle: FHIRBundle): {\n observations: FHIRObservation[];\n skipped: SkippedEntry[];\n} {\n const observations: FHIRObservation[] = [];\n const skipped: SkippedEntry[] = [];\n\n for (let i = 0; i < bundle.entry.length; i++) {\n const entry = bundle.entry[i]!;\n if (!entry.resource) {\n skipped.push({ index: i, reason: 'Entry has no resource' });\n continue;\n }\n\n if (entry.resource.resourceType !== 'Observation') {\n // Non-observation resources are silently skipped (Patient, DiagnosticReport, etc.)\n continue;\n }\n\n if (observations.length >= MAX_OBSERVATIONS) {\n skipped.push({ index: i, reason: `Maximum of ${MAX_OBSERVATIONS} observations exceeded` });\n continue;\n }\n\n observations.push(entry.resource as FHIRObservation);\n }\n\n return { observations, skipped };\n}\n\n/**\n * Map a FHIR Observation to internal format using LOINC→biomarker code lookup\n */\nexport function mapFHIRObservationToInternal(\n observation: FHIRObservation,\n index: number,\n): { observation: ImportedObservation } | { skipped: SkippedEntry } {\n const { internalCode, loincCode, reason } = resolveBiomarkerCode(observation);\n\n if (!internalCode) {\n return {\n skipped: { index, loincCode, reason, resourceType: 'Observation' },\n };\n }\n\n const definition = getDefinitionByCode(internalCode);\n\n // Extract value\n let value: number | string;\n let unit = '';\n let isQualitative = false;\n\n if (observation.valueQuantity?.value !== undefined) {\n value = observation.valueQuantity.value;\n unit = observation.valueQuantity.unit || observation.valueQuantity.code || '';\n } else if (observation.valueString) {\n value = observation.valueString;\n isQualitative = true;\n } else {\n return {\n skipped: {\n index,\n loincCode,\n reason: 'Observation has no value (valueQuantity or valueString)',\n resourceType: 'Observation',\n },\n };\n }\n\n // Extract collection date (effectiveDateTime or effectivePeriod.start)\n const collectionDate = observation.effectiveDateTime || observation.effectivePeriod?.start || '';\n if (!collectionDate) {\n return {\n skipped: {\n index,\n loincCode,\n reason: 'Observation has no effectiveDateTime or effectivePeriod.start',\n resourceType: 'Observation',\n },\n };\n }\n\n // Extract reference ranges\n let referenceMin: number | undefined;\n let referenceMax: number | undefined;\n if (observation.referenceRange?.[0]) {\n referenceMin = observation.referenceRange[0].low?.value;\n referenceMax = observation.referenceRange[0].high?.value;\n }\n\n const imported: ImportedObservation = {\n biomarkerCode: internalCode,\n biomarkerName:\n definition?.names.pt[0] || definition?.names.en[0] || observation.code.text || internalCode,\n collectionDate,\n flag: extractFlag(observation),\n isQualitative,\n loincCode,\n referenceMax,\n referenceMin,\n unit: unit || definition?.unit || '',\n value,\n };\n\n return { observation: imported };\n}\n\n/**\n * Process a complete FHIR Bundle for import\n */\nexport function processImportBundle(data: unknown): FHIRImportResult {\n // Structural validation\n const validationErrors = validateFHIRImportBundle(data);\n if (validationErrors.length > 0) {\n return {\n errors: validationErrors,\n imported: [],\n skipped: [],\n totalProcessed: 0,\n };\n }\n\n const bundle = data as FHIRBundle;\n\n // Extract observations\n const { observations, skipped } = extractObservationsFromBundle(bundle);\n\n // Map each observation to internal format\n const imported: ImportedObservation[] = [];\n const allSkipped: SkippedEntry[] = [...skipped];\n\n for (let i = 0; i < observations.length; i++) {\n const result = mapFHIRObservationToInternal(observations[i]!, i);\n\n if ('observation' in result) {\n imported.push(result.observation);\n } else {\n allSkipped.push(result.skipped);\n }\n }\n\n return {\n errors: [],\n imported,\n skipped: allSkipped,\n totalProcessed: observations.length,\n };\n}\n\nexport { MAX_FILE_SIZE, MAX_OBSERVATIONS };\n"]}
@@ -0,0 +1,9 @@
1
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true});// src/code-systems.ts
2
+ var LOINC_SYSTEM = "http://loinc.org";
3
+ var BIOMARKER_CODE_SYSTEM = "http://fhir-brasil.dev/biomarker-codes";
4
+
5
+
6
+
7
+
8
+ exports.LOINC_SYSTEM = LOINC_SYSTEM; exports.BIOMARKER_CODE_SYSTEM = BIOMARKER_CODE_SYSTEM;
9
+ //# sourceMappingURL=chunk-OR67NJDZ.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/home/runner/work/fhir-brasil/fhir-brasil/packages/core/dist/chunk-OR67NJDZ.cjs","../src/code-systems.ts"],"names":[],"mappings":"AAAA;ACSO,IAAM,aAAA,EAAe,kBAAA;AASrB,IAAM,sBAAA,EAAwB,wCAAA;ADfrC;AACA;AACE;AACA;AACF,2FAAC","file":"/home/runner/work/fhir-brasil/fhir-brasil/packages/core/dist/chunk-OR67NJDZ.cjs","sourcesContent":[null,"/**\n * Identificadores de sistema de código usados nos recursos FHIR.\n *\n * Ficam num módulo próprio porque exportação e importação precisam concordar\n * literalmente: o importador procura o coding pelo `system`, e uma string\n * divergente de um lado faz o biomarcador desaparecer sem erro.\n */\n\n/** LOINC, o vocabulário oficial de exames laboratoriais. */\nexport const LOINC_SYSTEM = 'http://loinc.org';\n\n/**\n * Códigos internos do fhir-brasil.\n *\n * Cobre o que não tem LOINC publicado: composição corporal por DEXA,\n * densidade óssea e escore de cálcio, entre outros. É o que permite o ciclo\n * exportar/importar preservar esses biomarcadores.\n */\nexport const BIOMARKER_CODE_SYSTEM = 'http://fhir-brasil.dev/biomarker-codes';\n"]}
@@ -1,5 +1,9 @@
1
1
  "use strict";Object.defineProperty(exports, "__esModule", {value: true});
2
2
 
3
+
4
+ var _chunkOR67NJDZcjs = require('./chunk-OR67NJDZ.cjs');
5
+
6
+
3
7
  var _chunkNXOTXKVBcjs = require('./chunk-NXOTXKVB.cjs');
4
8
 
5
9
 
@@ -30,7 +34,7 @@ function interpretationDisplay(flag) {
30
34
  }
31
35
  }
32
36
  function labObservationToFHIR(observation, patientId, laboratoryName) {
33
- const loincCode = _chunkNXOTXKVBcjs.codeToLoinc.call(void 0, observation.biomarkerCode) || "99999-9";
37
+ const loincCode = _chunkNXOTXKVBcjs.codeToLoinc.call(void 0, observation.biomarkerCode);
34
38
  const sourceUnit = observation.unit || _chunkMJ254F5Kcjs.getDefaultUnit.call(void 0, observation.biomarkerCode) || observation.unit;
35
39
  const ucumUnit = _chunkMJ254F5Kcjs.unitToUCUM.call(void 0, sourceUnit);
36
40
  const isQualitative = observation.isQualitative || typeof observation.value === "string";
@@ -47,16 +51,23 @@ function labObservationToFHIR(observation, patientId, laboratoryName) {
47
51
  }
48
52
  ],
49
53
  code: {
54
+ // Sem LOINC, o coding LOINC simplesmente não sai. Antes ia `99999-9`,
55
+ // que não é código LOINC nenhum: publicava sob `http://loinc.org` uma
56
+ // afirmação falsa, e quem consumisse o bundle confiando no system
57
+ // trataria aquilo como código de verdade. Composição corporal, densidade
58
+ // óssea e escore de cálcio não têm LOINC, e o certo é a lacuna explícita.
50
59
  coding: [
51
- {
52
- code: loincCode,
53
- display: observation.biomarkerName,
54
- system: "http://loinc.org"
55
- },
60
+ ...loincCode ? [
61
+ {
62
+ code: loincCode,
63
+ display: observation.biomarkerName,
64
+ system: _chunkOR67NJDZcjs.LOINC_SYSTEM
65
+ }
66
+ ] : [],
56
67
  {
57
68
  code: observation.biomarkerCode,
58
69
  display: observation.biomarkerName,
59
- system: "http://fhir-brasil.dev/biomarker-codes"
70
+ system: _chunkOR67NJDZcjs.BIOMARKER_CODE_SYSTEM
60
71
  }
61
72
  ],
62
73
  text: observation.biomarkerName
@@ -248,4 +259,4 @@ function labResultToFHIRBundle(report, observations, userProfile) {
248
259
 
249
260
 
250
261
  exports.labObservationToFHIR = labObservationToFHIR; exports.labReportToFHIR = labReportToFHIR; exports.userProfileToFHIR = userProfileToFHIR; exports.labResultToFHIRBundle = labResultToFHIRBundle;
251
- //# sourceMappingURL=chunk-2KDLOCD2.cjs.map
262
+ //# sourceMappingURL=chunk-QJF5BZ3J.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["/home/runner/work/fhir-brasil/fhir-brasil/packages/core/dist/chunk-QJF5BZ3J.cjs","../src/converter.ts"],"names":[],"mappings":"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","file":"/home/runner/work/fhir-brasil/fhir-brasil/packages/core/dist/chunk-QJF5BZ3J.cjs","sourcesContent":[null,"/**\n * FHIR Converter\n *\n * Converts lab results to FHIR R4 DiagnosticReport and Observation resources.\n * See: https://hl7.org/fhir/diagnosticreport.html\n */\n\nimport { codeToLoinc } from './biomarkers';\nimport { BIOMARKER_CODE_SYSTEM, LOINC_SYSTEM } from './code-systems';\nimport type { FHIRBundle, FHIRDiagnosticReport, FHIRObservation, FHIRPatient } from './fhir-types';\nimport type { Flag, LabObservationData, LabReportData, UserProfileData } from './types';\nimport { getDefaultUnit, unitToUCUM } from './units';\n\n// Re-export all types and functions\nexport * from './fhir-types';\n\n/**\n * Convert Flag to FHIR interpretation code\n */\nfunction interpretationCode(flag: Flag): string {\n switch (flag) {\n case 'H':\n return 'H'; // High\n case 'L':\n return 'L'; // Low\n default:\n return 'N'; // Normal\n }\n}\n\n/**\n * Convert Flag to FHIR interpretation display\n */\nfunction interpretationDisplay(flag: Flag): string {\n switch (flag) {\n case 'H':\n return 'High';\n case 'L':\n return 'Low';\n default:\n return 'Normal';\n }\n}\n\n/**\n * Convert generic lab observation to FHIR Observation\n */\nexport function labObservationToFHIR(\n observation: LabObservationData,\n patientId: string,\n laboratoryName?: string,\n): FHIRObservation {\n const loincCode = codeToLoinc(observation.biomarkerCode);\n // Use default unit if source unit is empty\n const sourceUnit =\n observation.unit || getDefaultUnit(observation.biomarkerCode) || observation.unit;\n const ucumUnit = unitToUCUM(sourceUnit);\n const isQualitative = observation.isQualitative || typeof observation.value === 'string';\n\n // Base observation structure\n const fhirObs: FHIRObservation = {\n category: [\n {\n coding: [\n {\n code: 'laboratory',\n display: 'Laboratory',\n system: 'http://terminology.hl7.org/CodeSystem/observation-category',\n },\n ],\n },\n ],\n code: {\n // Sem LOINC, o coding LOINC simplesmente não sai. Antes ia `99999-9`,\n // que não é código LOINC nenhum: publicava sob `http://loinc.org` uma\n // afirmação falsa, e quem consumisse o bundle confiando no system\n // trataria aquilo como código de verdade. Composição corporal, densidade\n // óssea e escore de cálcio não têm LOINC, e o certo é a lacuna explícita.\n coding: [\n ...(loincCode\n ? [\n {\n code: loincCode,\n display: observation.biomarkerName,\n system: LOINC_SYSTEM,\n },\n ]\n : []),\n {\n code: observation.biomarkerCode,\n display: observation.biomarkerName,\n system: BIOMARKER_CODE_SYSTEM,\n },\n ],\n text: observation.biomarkerName,\n },\n effectiveDateTime: observation.collectionDate,\n id: `${observation.reportId}-${observation.biomarkerCode}`,\n interpretation: [\n {\n coding: [\n {\n code: interpretationCode(observation.flag),\n display: interpretationDisplay(observation.flag),\n system: 'http://terminology.hl7.org/CodeSystem/v3-ObservationInterpretation',\n },\n ],\n },\n ],\n performer: laboratoryName ? [{ display: laboratoryName }] : undefined,\n resourceType: 'Observation',\n status: 'final',\n subject: {\n reference: `Patient/${patientId}`,\n },\n };\n\n // Add value based on type (qualitative = string, quantitative = number)\n if (isQualitative) {\n fhirObs.valueString = String(observation.value);\n } else {\n fhirObs.valueQuantity = {\n code: ucumUnit,\n system: 'http://unitsofmeasure.org',\n unit: sourceUnit,\n value: observation.value as number,\n };\n\n // Reference range only applies to quantitative values\n if (observation.referenceMin !== undefined && observation.referenceMax !== undefined) {\n fhirObs.referenceRange = [\n {\n high: {\n code: ucumUnit,\n system: 'http://unitsofmeasure.org',\n unit: sourceUnit,\n value: observation.referenceMax,\n },\n low: {\n code: ucumUnit,\n system: 'http://unitsofmeasure.org',\n unit: sourceUnit,\n value: observation.referenceMin,\n },\n },\n ];\n }\n }\n\n return fhirObs;\n}\n\n/**\n * Convert generic lab report to FHIR DiagnosticReport\n */\nexport function labReportToFHIR(\n report: LabReportData,\n patientId: string,\n observationIds: string[],\n): FHIRDiagnosticReport {\n // Map processing status to FHIR status\n let status: FHIRDiagnosticReport['status'];\n switch (report.processingStatus) {\n case 'complete':\n status = 'final';\n break;\n case 'partial':\n status = 'partial';\n break;\n case 'pending_review':\n status = 'preliminary';\n break;\n default:\n status = 'final';\n }\n\n return {\n category: [\n {\n coding: [\n {\n code: 'LAB',\n display: 'Laboratory',\n system: 'http://terminology.hl7.org/CodeSystem/v2-0074',\n },\n ],\n },\n ],\n code: {\n coding: [\n {\n code: '11502-2', // Laboratory report\n display: 'Laboratory report',\n system: 'http://loinc.org',\n },\n ],\n text: 'Laboratory Results',\n },\n conclusion:\n report.overallStatus === 'NORMAL'\n ? 'All results within normal limits'\n : 'One or more abnormal results detected',\n conclusionCode:\n report.overallStatus === 'ANORMAL'\n ? [\n {\n coding: [\n {\n code: 'A',\n display: 'Abnormal',\n system: 'http://terminology.hl7.org/CodeSystem/v3-ObservationInterpretation',\n },\n ],\n },\n ]\n : undefined,\n effectiveDateTime: report.collectionDate,\n id: report.reportId,\n issued: report.createdAt,\n performer: report.laboratoryName ? [{ display: report.laboratoryName }] : undefined,\n resourceType: 'DiagnosticReport',\n result: observationIds.map((id) => ({ reference: `Observation/${id}` })),\n status,\n subject: {\n reference: `Patient/${patientId}`,\n },\n };\n}\n\n/**\n * Convert user profile to FHIR Patient\n * NOTE: CPF is intentionally excluded for privacy (LGPD compliance)\n */\nexport function userProfileToFHIR(profile: UserProfileData): FHIRPatient {\n const nameParts = profile.name.split(' ');\n const given = nameParts.slice(0, -1);\n const family = nameParts[nameParts.length - 1] || '';\n\n return {\n address: profile.address\n ? [\n {\n city: profile.address.city,\n country: profile.address.country || 'BR',\n line: [\n profile.address.street && profile.address.number\n ? `${profile.address.street}, ${profile.address.number}`\n : profile.address.street,\n profile.address.complement,\n ].filter(Boolean) as string[],\n postalCode: profile.address.postalCode,\n state: profile.address.state,\n },\n ]\n : undefined,\n birthDate: profile.birthDate,\n gender: profile.gender,\n id: profile.userId,\n name: [\n {\n family,\n given: given.length > 0 ? given : undefined,\n text: profile.name,\n },\n ],\n resourceType: 'Patient',\n telecom:\n [\n ...(profile.email ? [{ system: 'email' as const, value: profile.email }] : []),\n ...(profile.phone ? [{ system: 'phone' as const, value: profile.phone }] : []),\n ].length > 0\n ? [\n ...(profile.email ? [{ system: 'email' as const, value: profile.email }] : []),\n ...(profile.phone ? [{ system: 'phone' as const, value: profile.phone }] : []),\n ]\n : undefined,\n };\n}\n\n/**\n * Convert complete lab result to FHIR Bundle\n * This is the main function for exporting lab results to FHIR R4 format\n */\nexport function labResultToFHIRBundle(\n report: LabReportData,\n observations: LabObservationData[],\n userProfile: UserProfileData,\n): FHIRBundle {\n const patientId = userProfile.userId;\n\n // Convert observations\n const fhirObservations = observations.map((obs) => ({\n fullUrl: `urn:uuid:observation-${obs.reportId}-${obs.biomarkerCode}`,\n resource: labObservationToFHIR(\n { ...obs, collectionDate: report.collectionDate },\n patientId,\n report.laboratoryName,\n ),\n }));\n\n const observationIds = observations.map(\n (obs) => `observation-${obs.reportId}-${obs.biomarkerCode}`,\n );\n\n // Convert report\n const diagnosticReport = labReportToFHIR(report, patientId, observationIds);\n\n // Convert patient\n const fhirPatient = userProfileToFHIR(userProfile);\n\n return {\n entry: [\n {\n fullUrl: `urn:uuid:${patientId}`,\n resource: fhirPatient,\n },\n {\n fullUrl: `urn:uuid:diagnostic-report-${report.reportId}`,\n resource: diagnosticReport,\n },\n ...fhirObservations,\n ],\n resourceType: 'Bundle',\n type: 'collection',\n };\n}\n"]}
@@ -1,3 +1,7 @@
1
+ import {
2
+ BIOMARKER_CODE_SYSTEM,
3
+ LOINC_SYSTEM
4
+ } from "./chunk-A6HR4XDK.js";
1
5
  import {
2
6
  codeToLoinc
3
7
  } from "./chunk-3KIJIRCI.js";
@@ -30,7 +34,7 @@ function interpretationDisplay(flag) {
30
34
  }
31
35
  }
32
36
  function labObservationToFHIR(observation, patientId, laboratoryName) {
33
- const loincCode = codeToLoinc(observation.biomarkerCode) || "99999-9";
37
+ const loincCode = codeToLoinc(observation.biomarkerCode);
34
38
  const sourceUnit = observation.unit || getDefaultUnit(observation.biomarkerCode) || observation.unit;
35
39
  const ucumUnit = unitToUCUM(sourceUnit);
36
40
  const isQualitative = observation.isQualitative || typeof observation.value === "string";
@@ -47,16 +51,23 @@ function labObservationToFHIR(observation, patientId, laboratoryName) {
47
51
  }
48
52
  ],
49
53
  code: {
54
+ // Sem LOINC, o coding LOINC simplesmente não sai. Antes ia `99999-9`,
55
+ // que não é código LOINC nenhum: publicava sob `http://loinc.org` uma
56
+ // afirmação falsa, e quem consumisse o bundle confiando no system
57
+ // trataria aquilo como código de verdade. Composição corporal, densidade
58
+ // óssea e escore de cálcio não têm LOINC, e o certo é a lacuna explícita.
50
59
  coding: [
51
- {
52
- code: loincCode,
53
- display: observation.biomarkerName,
54
- system: "http://loinc.org"
55
- },
60
+ ...loincCode ? [
61
+ {
62
+ code: loincCode,
63
+ display: observation.biomarkerName,
64
+ system: LOINC_SYSTEM
65
+ }
66
+ ] : [],
56
67
  {
57
68
  code: observation.biomarkerCode,
58
69
  display: observation.biomarkerName,
59
- system: "http://fhir-brasil.dev/biomarker-codes"
70
+ system: BIOMARKER_CODE_SYSTEM
60
71
  }
61
72
  ],
62
73
  text: observation.biomarkerName
@@ -248,4 +259,4 @@ export {
248
259
  userProfileToFHIR,
249
260
  labResultToFHIRBundle
250
261
  };
251
- //# sourceMappingURL=chunk-6TQ24JCA.js.map
262
+ //# sourceMappingURL=chunk-SN5ZJHWS.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/converter.ts"],"sourcesContent":["/**\n * FHIR Converter\n *\n * Converts lab results to FHIR R4 DiagnosticReport and Observation resources.\n * See: https://hl7.org/fhir/diagnosticreport.html\n */\n\nimport { codeToLoinc } from './biomarkers';\nimport { BIOMARKER_CODE_SYSTEM, LOINC_SYSTEM } from './code-systems';\nimport type { FHIRBundle, FHIRDiagnosticReport, FHIRObservation, FHIRPatient } from './fhir-types';\nimport type { Flag, LabObservationData, LabReportData, UserProfileData } from './types';\nimport { getDefaultUnit, unitToUCUM } from './units';\n\n// Re-export all types and functions\nexport * from './fhir-types';\n\n/**\n * Convert Flag to FHIR interpretation code\n */\nfunction interpretationCode(flag: Flag): string {\n switch (flag) {\n case 'H':\n return 'H'; // High\n case 'L':\n return 'L'; // Low\n default:\n return 'N'; // Normal\n }\n}\n\n/**\n * Convert Flag to FHIR interpretation display\n */\nfunction interpretationDisplay(flag: Flag): string {\n switch (flag) {\n case 'H':\n return 'High';\n case 'L':\n return 'Low';\n default:\n return 'Normal';\n }\n}\n\n/**\n * Convert generic lab observation to FHIR Observation\n */\nexport function labObservationToFHIR(\n observation: LabObservationData,\n patientId: string,\n laboratoryName?: string,\n): FHIRObservation {\n const loincCode = codeToLoinc(observation.biomarkerCode);\n // Use default unit if source unit is empty\n const sourceUnit =\n observation.unit || getDefaultUnit(observation.biomarkerCode) || observation.unit;\n const ucumUnit = unitToUCUM(sourceUnit);\n const isQualitative = observation.isQualitative || typeof observation.value === 'string';\n\n // Base observation structure\n const fhirObs: FHIRObservation = {\n category: [\n {\n coding: [\n {\n code: 'laboratory',\n display: 'Laboratory',\n system: 'http://terminology.hl7.org/CodeSystem/observation-category',\n },\n ],\n },\n ],\n code: {\n // Sem LOINC, o coding LOINC simplesmente não sai. Antes ia `99999-9`,\n // que não é código LOINC nenhum: publicava sob `http://loinc.org` uma\n // afirmação falsa, e quem consumisse o bundle confiando no system\n // trataria aquilo como código de verdade. Composição corporal, densidade\n // óssea e escore de cálcio não têm LOINC, e o certo é a lacuna explícita.\n coding: [\n ...(loincCode\n ? [\n {\n code: loincCode,\n display: observation.biomarkerName,\n system: LOINC_SYSTEM,\n },\n ]\n : []),\n {\n code: observation.biomarkerCode,\n display: observation.biomarkerName,\n system: BIOMARKER_CODE_SYSTEM,\n },\n ],\n text: observation.biomarkerName,\n },\n effectiveDateTime: observation.collectionDate,\n id: `${observation.reportId}-${observation.biomarkerCode}`,\n interpretation: [\n {\n coding: [\n {\n code: interpretationCode(observation.flag),\n display: interpretationDisplay(observation.flag),\n system: 'http://terminology.hl7.org/CodeSystem/v3-ObservationInterpretation',\n },\n ],\n },\n ],\n performer: laboratoryName ? [{ display: laboratoryName }] : undefined,\n resourceType: 'Observation',\n status: 'final',\n subject: {\n reference: `Patient/${patientId}`,\n },\n };\n\n // Add value based on type (qualitative = string, quantitative = number)\n if (isQualitative) {\n fhirObs.valueString = String(observation.value);\n } else {\n fhirObs.valueQuantity = {\n code: ucumUnit,\n system: 'http://unitsofmeasure.org',\n unit: sourceUnit,\n value: observation.value as number,\n };\n\n // Reference range only applies to quantitative values\n if (observation.referenceMin !== undefined && observation.referenceMax !== undefined) {\n fhirObs.referenceRange = [\n {\n high: {\n code: ucumUnit,\n system: 'http://unitsofmeasure.org',\n unit: sourceUnit,\n value: observation.referenceMax,\n },\n low: {\n code: ucumUnit,\n system: 'http://unitsofmeasure.org',\n unit: sourceUnit,\n value: observation.referenceMin,\n },\n },\n ];\n }\n }\n\n return fhirObs;\n}\n\n/**\n * Convert generic lab report to FHIR DiagnosticReport\n */\nexport function labReportToFHIR(\n report: LabReportData,\n patientId: string,\n observationIds: string[],\n): FHIRDiagnosticReport {\n // Map processing status to FHIR status\n let status: FHIRDiagnosticReport['status'];\n switch (report.processingStatus) {\n case 'complete':\n status = 'final';\n break;\n case 'partial':\n status = 'partial';\n break;\n case 'pending_review':\n status = 'preliminary';\n break;\n default:\n status = 'final';\n }\n\n return {\n category: [\n {\n coding: [\n {\n code: 'LAB',\n display: 'Laboratory',\n system: 'http://terminology.hl7.org/CodeSystem/v2-0074',\n },\n ],\n },\n ],\n code: {\n coding: [\n {\n code: '11502-2', // Laboratory report\n display: 'Laboratory report',\n system: 'http://loinc.org',\n },\n ],\n text: 'Laboratory Results',\n },\n conclusion:\n report.overallStatus === 'NORMAL'\n ? 'All results within normal limits'\n : 'One or more abnormal results detected',\n conclusionCode:\n report.overallStatus === 'ANORMAL'\n ? [\n {\n coding: [\n {\n code: 'A',\n display: 'Abnormal',\n system: 'http://terminology.hl7.org/CodeSystem/v3-ObservationInterpretation',\n },\n ],\n },\n ]\n : undefined,\n effectiveDateTime: report.collectionDate,\n id: report.reportId,\n issued: report.createdAt,\n performer: report.laboratoryName ? [{ display: report.laboratoryName }] : undefined,\n resourceType: 'DiagnosticReport',\n result: observationIds.map((id) => ({ reference: `Observation/${id}` })),\n status,\n subject: {\n reference: `Patient/${patientId}`,\n },\n };\n}\n\n/**\n * Convert user profile to FHIR Patient\n * NOTE: CPF is intentionally excluded for privacy (LGPD compliance)\n */\nexport function userProfileToFHIR(profile: UserProfileData): FHIRPatient {\n const nameParts = profile.name.split(' ');\n const given = nameParts.slice(0, -1);\n const family = nameParts[nameParts.length - 1] || '';\n\n return {\n address: profile.address\n ? [\n {\n city: profile.address.city,\n country: profile.address.country || 'BR',\n line: [\n profile.address.street && profile.address.number\n ? `${profile.address.street}, ${profile.address.number}`\n : profile.address.street,\n profile.address.complement,\n ].filter(Boolean) as string[],\n postalCode: profile.address.postalCode,\n state: profile.address.state,\n },\n ]\n : undefined,\n birthDate: profile.birthDate,\n gender: profile.gender,\n id: profile.userId,\n name: [\n {\n family,\n given: given.length > 0 ? given : undefined,\n text: profile.name,\n },\n ],\n resourceType: 'Patient',\n telecom:\n [\n ...(profile.email ? [{ system: 'email' as const, value: profile.email }] : []),\n ...(profile.phone ? [{ system: 'phone' as const, value: profile.phone }] : []),\n ].length > 0\n ? [\n ...(profile.email ? [{ system: 'email' as const, value: profile.email }] : []),\n ...(profile.phone ? [{ system: 'phone' as const, value: profile.phone }] : []),\n ]\n : undefined,\n };\n}\n\n/**\n * Convert complete lab result to FHIR Bundle\n * This is the main function for exporting lab results to FHIR R4 format\n */\nexport function labResultToFHIRBundle(\n report: LabReportData,\n observations: LabObservationData[],\n userProfile: UserProfileData,\n): FHIRBundle {\n const patientId = userProfile.userId;\n\n // Convert observations\n const fhirObservations = observations.map((obs) => ({\n fullUrl: `urn:uuid:observation-${obs.reportId}-${obs.biomarkerCode}`,\n resource: labObservationToFHIR(\n { ...obs, collectionDate: report.collectionDate },\n patientId,\n report.laboratoryName,\n ),\n }));\n\n const observationIds = observations.map(\n (obs) => `observation-${obs.reportId}-${obs.biomarkerCode}`,\n );\n\n // Convert report\n const diagnosticReport = labReportToFHIR(report, patientId, observationIds);\n\n // Convert patient\n const fhirPatient = userProfileToFHIR(userProfile);\n\n return {\n entry: [\n {\n fullUrl: `urn:uuid:${patientId}`,\n resource: fhirPatient,\n },\n {\n fullUrl: `urn:uuid:diagnostic-report-${report.reportId}`,\n resource: diagnosticReport,\n },\n ...fhirObservations,\n ],\n resourceType: 'Bundle',\n type: 'collection',\n };\n}\n"],"mappings":"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","names":[]}
@@ -1,6 +1,13 @@
1
1
  import {
2
- getDefinitionByLoinc,
3
- loincToCode
2
+ BIOMARKER_CODE_SYSTEM,
3
+ LOINC_SYSTEM
4
+ } from "./chunk-A6HR4XDK.js";
5
+ import {
6
+ codeToLoinc,
7
+ getDefinitionByCode,
8
+ isValidCode,
9
+ loincToCode,
10
+ normalizeCode
4
11
  } from "./chunk-3KIJIRCI.js";
5
12
  import {
6
13
  validateFHIRImportBundle
@@ -9,10 +16,30 @@ import {
9
16
  // src/importer.ts
10
17
  var MAX_OBSERVATIONS = 5e3;
11
18
  var MAX_FILE_SIZE = 15 * 1024 * 1024;
12
- function extractLoincCode(observation) {
13
- if (!observation.code?.coding) return void 0;
14
- const loincCoding = observation.code.coding.find((c) => c.system === "http://loinc.org");
15
- return loincCoding?.code;
19
+ function resolveBiomarkerCode(observation) {
20
+ const coding = observation.code?.coding ?? [];
21
+ const loincCode = coding.find((c) => c.system === LOINC_SYSTEM)?.code;
22
+ const declaredCode = coding.find((c) => c.system === BIOMARKER_CODE_SYSTEM)?.code;
23
+ const seenCodes = [
24
+ ...loincCode ? [`LOINC ${loincCode}`] : [],
25
+ ...declaredCode ? [`biomarker code ${declaredCode}`] : []
26
+ ];
27
+ let reason;
28
+ if (seenCodes.length > 0) {
29
+ reason = `Unknown code: ${seenCodes.join(", ")}`;
30
+ } else if (coding.length > 0) {
31
+ const systems = [...new Set(coding.map((c) => c.system ?? "(sem system)"))];
32
+ reason = `No code in a supported system (found: ${systems.join(", ")})`;
33
+ } else {
34
+ reason = "No code found in observation coding";
35
+ }
36
+ const fromLoinc = loincCode ? loincToCode(loincCode) : void 0;
37
+ if (fromLoinc) return { internalCode: fromLoinc, loincCode, reason };
38
+ if (declaredCode && isValidCode(declaredCode)) {
39
+ const canonical = normalizeCode(declaredCode);
40
+ return { internalCode: canonical, loincCode: codeToLoinc(canonical), reason };
41
+ }
42
+ return { loincCode, reason };
16
43
  }
17
44
  function extractFlag(observation) {
18
45
  const code = observation.interpretation?.[0]?.coding?.[0]?.code;
@@ -41,28 +68,13 @@ function extractObservationsFromBundle(bundle) {
41
68
  return { observations, skipped };
42
69
  }
43
70
  function mapFHIRObservationToInternal(observation, index) {
44
- const loincCode = extractLoincCode(observation);
45
- if (!loincCode) {
46
- return {
47
- skipped: {
48
- index,
49
- reason: "No LOINC code found in observation coding",
50
- resourceType: "Observation"
51
- }
52
- };
53
- }
54
- const internalCode = loincToCode(loincCode);
71
+ const { internalCode, loincCode, reason } = resolveBiomarkerCode(observation);
55
72
  if (!internalCode) {
56
73
  return {
57
- skipped: {
58
- index,
59
- loincCode,
60
- reason: `Unknown LOINC code: ${loincCode}`,
61
- resourceType: "Observation"
62
- }
74
+ skipped: { index, loincCode, reason, resourceType: "Observation" }
63
75
  };
64
76
  }
65
- const definition = getDefinitionByLoinc(loincCode);
77
+ const definition = getDefinitionByCode(internalCode);
66
78
  let value;
67
79
  let unit = "";
68
80
  let isQualitative = false;
@@ -150,4 +162,4 @@ export {
150
162
  mapFHIRObservationToInternal,
151
163
  processImportBundle
152
164
  };
153
- //# sourceMappingURL=chunk-OZGOBYLX.js.map
165
+ //# sourceMappingURL=chunk-YYANW65U.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/importer.ts"],"sourcesContent":["/**\n * FHIR Importer\n *\n * Parses FHIR R4 Bundles and extracts Observation resources with known LOINC codes,\n * mapping them to internal biomarker codes for storage as lab results.\n */\n\nimport {\n codeToLoinc,\n getDefinitionByCode,\n isValidCode,\n loincToCode,\n normalizeCode,\n} from './biomarkers';\nimport { BIOMARKER_CODE_SYSTEM, LOINC_SYSTEM } from './code-systems';\nimport type { FHIRBundle, FHIRObservation } from './fhir-types';\nimport { validateFHIRImportBundle } from './validators';\n\nexport interface ImportedObservation {\n biomarkerCode: string;\n biomarkerName: string;\n collectionDate: string;\n flag: 'H' | 'L' | '';\n isQualitative: boolean;\n /** Ausente nos biomarcadores sem LOINC publicado, como composição corporal. */\n loincCode?: string;\n referenceMax?: number;\n referenceMin?: number;\n unit: string;\n value: number | string;\n}\n\nexport interface SkippedEntry {\n index: number;\n loincCode?: string;\n reason: string;\n resourceType?: string;\n}\n\nexport interface ImportError {\n details: string;\n field: string;\n}\n\nexport interface FHIRImportResult {\n errors: ImportError[];\n imported: ImportedObservation[];\n skipped: SkippedEntry[];\n totalProcessed: number;\n}\n\n/**\n * Limites de importação.\n *\n * Uma Observation exportada ocupa cerca de 1,25KB em JSON compacto e 2,75KB\n * quando o arquivo vem indentado, medido sobre um histórico real de 998\n * Observations em 61 laudos. Nesse tamanho, 5000 Observations dão 6,0MB\n * compactos ou 13,1MB indentados, e por isso o teto de arquivo é 15MB: cobre\n * as duas formas com folga.\n *\n * Na densidade desse mesmo histórico (16 Observations por laudo), 5000\n * equivalem a cerca de 300 laudos.\n */\nconst MAX_OBSERVATIONS = 5000;\nconst MAX_FILE_SIZE = 15 * 1024 * 1024; // 15MB\n\n/**\n * Resolve o código interno do biomarcador a partir do `code.coding`.\n *\n * LOINC primeiro, que é o vocabulário que arquivos de terceiros usam. Quando\n * não resolve, cai para o coding de códigos internos, presente nos arquivos\n * exportados pela própria plataforma.\n *\n * O fallback cobre dois casos: biomarcadores sem LOINC publicado (composição\n * corporal, densidade óssea, escore de cálcio) e arquivos antigos, exportados\n * quando esses biomarcadores saíam com o placeholder `99999-9`, que não\n * resolve para nada.\n */\nfunction resolveBiomarkerCode(observation: FHIRObservation): {\n internalCode?: string;\n loincCode?: string;\n reason: string;\n} {\n const coding = observation.code?.coding ?? [];\n const loincCode = coding.find((c) => c.system === LOINC_SYSTEM)?.code;\n const declaredCode = coding.find((c) => c.system === BIOMARKER_CODE_SYSTEM)?.code;\n\n const seenCodes = [\n ...(loincCode ? [`LOINC ${loincCode}`] : []),\n ...(declaredCode ? [`biomarker code ${declaredCode}`] : []),\n ];\n\n // Três motivos distintos de descarte, que antes se confundiam num só. Sem\n // essa separação, um arquivo em SNOMED relatava \"nenhum código encontrado\",\n // sugerindo `coding` vazio quando na verdade o código existia e estava num\n // system que não tratamos. Para importação de terceiros, é a diferença entre\n // um diagnóstico acionável e um enigma.\n let reason: string;\n if (seenCodes.length > 0) {\n reason = `Unknown code: ${seenCodes.join(', ')}`;\n } else if (coding.length > 0) {\n const systems = [...new Set(coding.map((c) => c.system ?? '(sem system)'))];\n reason = `No code in a supported system (found: ${systems.join(', ')})`;\n } else {\n reason = 'No code found in observation coding';\n }\n\n const fromLoinc = loincCode ? loincToCode(loincCode) : undefined;\n if (fromLoinc) return { internalCode: fromLoinc, loincCode, reason };\n\n if (declaredCode && isValidCode(declaredCode)) {\n // `isValidCode` aceita alias, mas alias não serve como código armazenado:\n // 49 definições têm um, e tanto `codeToLoinc` quanto as faixas de\n // referência são indexadas só pelo canônico. Sem normalizar, `VLDL_Cholesterol`\n // entraria no lugar de `VLDL` e perderia o LOINC 13458-5 que ele tem.\n // Importar um Bundle é fronteira de dados, que é onde `normalizeCode` deve\n // ser aplicado.\n const canonical = normalizeCode(declaredCode);\n\n // `codeToLoinc` devolve undefined para quem não tem LOINC, que é o caso\n // esperado aqui. O campo fica de fora em vez de receber um valor inventado.\n return { internalCode: canonical, loincCode: codeToLoinc(canonical), reason };\n }\n\n return { loincCode, reason };\n}\n\n/**\n * Extract interpretation flag from Observation\n */\nfunction extractFlag(observation: FHIRObservation): 'H' | 'L' | '' {\n const code = observation.interpretation?.[0]?.coding?.[0]?.code;\n if (code === 'H' || code === 'HH') return 'H';\n if (code === 'L' || code === 'LL') return 'L';\n return '';\n}\n\n/**\n * Extract Observation resources from a FHIR Bundle\n */\nexport function extractObservationsFromBundle(bundle: FHIRBundle): {\n observations: FHIRObservation[];\n skipped: SkippedEntry[];\n} {\n const observations: FHIRObservation[] = [];\n const skipped: SkippedEntry[] = [];\n\n for (let i = 0; i < bundle.entry.length; i++) {\n const entry = bundle.entry[i]!;\n if (!entry.resource) {\n skipped.push({ index: i, reason: 'Entry has no resource' });\n continue;\n }\n\n if (entry.resource.resourceType !== 'Observation') {\n // Non-observation resources are silently skipped (Patient, DiagnosticReport, etc.)\n continue;\n }\n\n if (observations.length >= MAX_OBSERVATIONS) {\n skipped.push({ index: i, reason: `Maximum of ${MAX_OBSERVATIONS} observations exceeded` });\n continue;\n }\n\n observations.push(entry.resource as FHIRObservation);\n }\n\n return { observations, skipped };\n}\n\n/**\n * Map a FHIR Observation to internal format using LOINC→biomarker code lookup\n */\nexport function mapFHIRObservationToInternal(\n observation: FHIRObservation,\n index: number,\n): { observation: ImportedObservation } | { skipped: SkippedEntry } {\n const { internalCode, loincCode, reason } = resolveBiomarkerCode(observation);\n\n if (!internalCode) {\n return {\n skipped: { index, loincCode, reason, resourceType: 'Observation' },\n };\n }\n\n const definition = getDefinitionByCode(internalCode);\n\n // Extract value\n let value: number | string;\n let unit = '';\n let isQualitative = false;\n\n if (observation.valueQuantity?.value !== undefined) {\n value = observation.valueQuantity.value;\n unit = observation.valueQuantity.unit || observation.valueQuantity.code || '';\n } else if (observation.valueString) {\n value = observation.valueString;\n isQualitative = true;\n } else {\n return {\n skipped: {\n index,\n loincCode,\n reason: 'Observation has no value (valueQuantity or valueString)',\n resourceType: 'Observation',\n },\n };\n }\n\n // Extract collection date (effectiveDateTime or effectivePeriod.start)\n const collectionDate = observation.effectiveDateTime || observation.effectivePeriod?.start || '';\n if (!collectionDate) {\n return {\n skipped: {\n index,\n loincCode,\n reason: 'Observation has no effectiveDateTime or effectivePeriod.start',\n resourceType: 'Observation',\n },\n };\n }\n\n // Extract reference ranges\n let referenceMin: number | undefined;\n let referenceMax: number | undefined;\n if (observation.referenceRange?.[0]) {\n referenceMin = observation.referenceRange[0].low?.value;\n referenceMax = observation.referenceRange[0].high?.value;\n }\n\n const imported: ImportedObservation = {\n biomarkerCode: internalCode,\n biomarkerName:\n definition?.names.pt[0] || definition?.names.en[0] || observation.code.text || internalCode,\n collectionDate,\n flag: extractFlag(observation),\n isQualitative,\n loincCode,\n referenceMax,\n referenceMin,\n unit: unit || definition?.unit || '',\n value,\n };\n\n return { observation: imported };\n}\n\n/**\n * Process a complete FHIR Bundle for import\n */\nexport function processImportBundle(data: unknown): FHIRImportResult {\n // Structural validation\n const validationErrors = validateFHIRImportBundle(data);\n if (validationErrors.length > 0) {\n return {\n errors: validationErrors,\n imported: [],\n skipped: [],\n totalProcessed: 0,\n };\n }\n\n const bundle = data as FHIRBundle;\n\n // Extract observations\n const { observations, skipped } = extractObservationsFromBundle(bundle);\n\n // Map each observation to internal format\n const imported: ImportedObservation[] = [];\n const allSkipped: SkippedEntry[] = [...skipped];\n\n for (let i = 0; i < observations.length; i++) {\n const result = mapFHIRObservationToInternal(observations[i]!, i);\n\n if ('observation' in result) {\n imported.push(result.observation);\n } else {\n allSkipped.push(result.skipped);\n }\n }\n\n return {\n errors: [],\n imported,\n skipped: allSkipped,\n totalProcessed: observations.length,\n };\n}\n\nexport { MAX_FILE_SIZE, MAX_OBSERVATIONS };\n"],"mappings":"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","names":[]}