fhirsmith 0.8.6 → 0.9.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.
@@ -56,7 +56,13 @@ const Extensions = {
56
56
  if (!resource) {
57
57
  return undefined;
58
58
  }
59
- const extensions = Array.isArray(resource) ? resource : (resource.extension || []);
59
+ let extensions = Array.isArray(resource) ? resource : (resource.extension || []);
60
+ for (let ext of extensions || []) {
61
+ if (ext.url === url) {
62
+ return getValuePrimitive(ext);
63
+ }
64
+ }
65
+ extensions = Array.isArray(resource) ? resource : (resource.modifierExtension || []);
60
66
  for (let ext of extensions || []) {
61
67
  if (ext.url === url) {
62
68
  return getValuePrimitive(ext);
@@ -466,14 +466,15 @@ class Renderer {
466
466
  li.tx(" "+ this.translate('VALUE_SET_WHERE')+" ");
467
467
  li.startCommaList("and");
468
468
  for (let f of inc.filter) {
469
- if (f.op == 'exists') {
469
+ let op = this.readFilterOp(f);
470
+ if (op == 'exists') {
470
471
  if (f.value == "true") {
471
472
  li.commaItem(f.property+" "+ this.translate('VALUE_SET_EXISTS'));
472
473
  } else {
473
474
  li.commaItem(f.property+" "+ this.translate('VALUE_SET_DOESNT_EXIST'));
474
475
  }
475
476
  } else {
476
- li.commaItem(f.property + " " + f.op + " ");
477
+ li.commaItem(f.property + " " + op + " ");
477
478
  const loc = this.linkResolver ? await this.linkResolver.resolveCode(this.opContext, inc.system, inc.version, f.value) : null;
478
479
  if (loc) {
479
480
  li.ah(loc.link).tx(loc.description);
@@ -2243,6 +2244,14 @@ class Renderer {
2243
2244
  return defn+' = ' + value;
2244
2245
  }
2245
2246
  }
2247
+
2248
+ readFilterOp(f) {
2249
+ if (f._op) {
2250
+ return Extensions.readString(f._op, 'http://hl7.org/fhir/5.0/StructureDefinition/extension-ValueSet.compose.include.filter.op');
2251
+ } else {
2252
+ return f.op;
2253
+ }
2254
+ }
2246
2255
  }
2247
2256
 
2248
2257
  module.exports = { Renderer };
@@ -10,6 +10,7 @@ const {
10
10
  BaseUnit, DefinedUnit, Prefix, Value, Term, Symbol, Factor, Canonical, CanonicalUnit,
11
11
  Registry
12
12
  } = require('./ucum-types.js');
13
+ const regexUtilities = require("../../library/regex-utilities");
13
14
 
14
15
  // Lexer for tokenizing UCUM expressions (port of Lexer.java)
15
16
  class Lexer {
@@ -763,7 +764,7 @@ class Search {
763
764
 
764
765
  if (isRegex) {
765
766
  try {
766
- const regex = new RegExp(text);
767
+ const regex = regexUtilities.compile(text);
767
768
  return regex.test(value);
768
769
  } catch (e) {
769
770
  this.log.error(e);
package/tx/ocl/cs-ocl.cjs CHANGED
@@ -13,6 +13,7 @@ const { OCLBackgroundJobQueue } = require('./jobs/background-queue');
13
13
  const { OCLConceptFilterContext } = require('./model/concept-filter-context');
14
14
  const { toConceptContext } = require('./mappers/concept-mapper');
15
15
  const { patchSearchWorkerForOCLCodeFiltering } = require('./shared/patches');
16
+ const regexUtilities = require("../../library/regex-utilities");
16
17
 
17
18
  patchSearchWorkerForOCLCodeFiltering();
18
19
 
@@ -735,6 +736,17 @@ class OCLSourceCodeSystemProvider extends CodeSystemProvider {
735
736
  return { context: this.conceptCache.get(code), message: null };
736
737
  }
737
738
 
739
+ // OCL concept IDs may differ in case from the FHIR code (e.g. "y" vs "Y").
740
+ // Try a case-insensitive cache lookup before hitting the network.
741
+ const codeLower = code.toLowerCase();
742
+ for (const [key, value] of this.conceptCache.entries()) {
743
+ if (key.toLowerCase() === codeLower) {
744
+ // Cache under the requested case as well so future lookups are O(1).
745
+ this.conceptCache.set(code, value);
746
+ return { context: value, message: null };
747
+ }
748
+ }
749
+
738
750
  if (this.scheduleBackgroundLoad) {
739
751
  this.scheduleBackgroundLoad('lookup-miss');
740
752
  }
@@ -1032,23 +1044,26 @@ class OCLSourceCodeSystemProvider extends CodeSystemProvider {
1032
1044
  this.scheduleBackgroundLoad('concept-miss');
1033
1045
  }
1034
1046
 
1035
- const url = this.#buildConceptUrl(code);
1036
1047
  const pending = (async () => {
1037
- let response;
1038
- try {
1039
- response = await this.httpClient.get(url, { params: { verbose: true } });
1040
- } catch (error) {
1041
- // Missing concept should be treated as not-found, not as an internal server failure.
1042
- if (error && error.response && error.response.status === 404) {
1043
- return null;
1044
- }
1045
- throw error;
1048
+ const concept = await this.#fetchConceptByCode(code);
1049
+ if (concept) {
1050
+ return concept;
1046
1051
  }
1047
- const concept = this.#toConceptContext(response.data);
1048
- if (concept && concept.code) {
1049
- this.conceptCache.set(concept.code, concept);
1052
+ // OCL concept IDs may differ in case from the FHIR code (e.g. "y" vs "Y").
1053
+ // Try common case alternatives before giving up.
1054
+ const lower = code.toLowerCase();
1055
+ const upper = code.toUpperCase();
1056
+ for (const alt of [lower, upper]) {
1057
+ if (alt !== code) {
1058
+ const altConcept = await this.#fetchConceptByCode(alt);
1059
+ if (altConcept) {
1060
+ // Cache under the originally requested code so future lookups hit directly.
1061
+ this.conceptCache.set(code, altConcept);
1062
+ return altConcept;
1063
+ }
1064
+ }
1050
1065
  }
1051
- return concept;
1066
+ return null;
1052
1067
  })();
1053
1068
 
1054
1069
  this.pendingConceptRequests.set(pendingKey, pending);
@@ -1059,6 +1074,24 @@ class OCLSourceCodeSystemProvider extends CodeSystemProvider {
1059
1074
  }
1060
1075
  }
1061
1076
 
1077
+ async #fetchConceptByCode(code) {
1078
+ const url = this.#buildConceptUrl(code);
1079
+ let response;
1080
+ try {
1081
+ response = await this.httpClient.get(url, { params: { verbose: true } });
1082
+ } catch (error) {
1083
+ if (error && error.response && error.response.status === 404) {
1084
+ return null;
1085
+ }
1086
+ throw error;
1087
+ }
1088
+ const concept = this.#toConceptContext(response.data);
1089
+ if (concept && concept.code) {
1090
+ this.conceptCache.set(concept.code, concept);
1091
+ }
1092
+ return concept;
1093
+ }
1094
+
1062
1095
  async #allConceptContexts() {
1063
1096
  const concepts = new Map();
1064
1097
 
@@ -1135,7 +1168,7 @@ class OCLSourceCodeSystemProvider extends CodeSystemProvider {
1135
1168
 
1136
1169
  #buildPropertyMatcher(prop, op, value) {
1137
1170
  if (op === 'regex') {
1138
- const regex = new RegExp(String(value), 'i');
1171
+ const regex = regexUtilities.compile(String(value), 'i');
1139
1172
  return concept => {
1140
1173
  const candidate = this.#valueForFilter(concept, prop);
1141
1174
  if (candidate == null) {
package/tx/ocl/vs-ocl.cjs CHANGED
@@ -389,20 +389,26 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
389
389
  }
390
390
 
391
391
  #indexValueSet(vs) {
392
- const existing = this.valueSetMap.get(vs.url)
393
- || (vs.version ? this.valueSetMap.get(`${vs.url}|${vs.version}`) : null)
394
- || this._idMap.get(vs.id)
395
- || null;
396
-
397
- // indexa se não existe ou se for o mesmo objeto
398
- if (!existing || existing === vs) {
399
- this.valueSetMap.set(vs.url, vs);
400
- if (vs.version) {
401
- this.valueSetMap.set(`${vs.url}|${vs.version}`, vs);
402
- }
403
- this.valueSetMap.set(vs.id, vs);
404
- this._idMap.set(vs.id, vs);
392
+ const existing = this.valueSetMap.get(vs.url) || null;
393
+
394
+ // When fresh discovery metadata replaces a cold-cached entry, carry over
395
+ // the enumerated compose so the expand engine doesn't fall back to
396
+ // "include whole CodeSystem". The background expansion will eventually
397
+ // refresh it with up-to-date collection contents.
398
+ if (existing && existing !== vs
399
+ && Array.isArray(existing.jsonObj?.compose?.include)
400
+ && existing.jsonObj.compose.include.some(inc => Array.isArray(inc.concept) && inc.concept.length > 0)
401
+ && (!vs.jsonObj.compose || !Array.isArray(vs.jsonObj.compose.include) || vs.jsonObj.compose.include.length === 0)
402
+ ) {
403
+ vs.jsonObj.compose = existing.jsonObj.compose;
404
+ }
405
+
406
+ this.valueSetMap.set(vs.url, vs);
407
+ if (vs.version) {
408
+ this.valueSetMap.set(`${vs.url}|${vs.version}`, vs);
405
409
  }
410
+ this.valueSetMap.set(vs.id, vs);
411
+ this._idMap.set(vs.id, vs);
406
412
  }
407
413
 
408
414
  #toValueSet(collection) {
@@ -568,6 +574,17 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
568
574
  ? vs.jsonObj.compose.include
569
575
  : [];
570
576
 
577
+ // If the compose already has enumerated concepts (from background expansion),
578
+ // it is the authoritative representation of the collection contents — don't
579
+ // overwrite it with system-only entries that would cause the expand engine
580
+ // to include ALL concepts from the CodeSystem.
581
+ const hasEnumeratedConcepts = existingInclude.some(
582
+ inc => Array.isArray(inc.concept) && inc.concept.length > 0
583
+ );
584
+ if (hasEnumeratedConcepts) {
585
+ return;
586
+ }
587
+
571
588
  // Always normalize existing compose entries first because discovery metadata
572
589
  // can carry non-canonical preferred_source values.
573
590
  const include = this.#normalizeComposeIncludes(existingInclude);
@@ -972,9 +989,12 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
972
989
  return;
973
990
  }
974
991
 
975
- const cached = this.backgroundExpansionCache.get(cacheKey);
992
+ let cached = this.backgroundExpansionCache.get(cacheKey);
993
+ let invalidated = false;
976
994
  if (cached && !this.#isCachedExpansionValid(vs, cached)) {
977
995
  this.backgroundExpansionCache.delete(cacheKey);
996
+ cached = null;
997
+ invalidated = true;
978
998
  }
979
999
 
980
1000
  // Already have a cached compose ready
@@ -982,23 +1002,22 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
982
1002
  return;
983
1003
  }
984
1004
 
985
- const cacheFilePath = getCacheFilePath(CACHE_VS_DIR, vs.url, vs.version || null, paramsKey);
986
- const cacheAgeFromFileMs = getColdCacheAgeMs(cacheFilePath);
987
- const persistedCache = this.backgroundExpansionCache.get(cacheKey);
988
- const cacheAgeFromMetadataMs = Number.isFinite(persistedCache?.createdAt)
989
- ? Math.max(0, Date.now() - persistedCache.createdAt)
990
- : null;
991
-
992
- // Treat cache as fresh when either file mtime or persisted timestamp is recent.
993
- const freshnessCandidates = [cacheAgeFromFileMs, cacheAgeFromMetadataMs].filter(age => age != null);
994
- const freshestCacheAgeMs = freshnessCandidates.length > 0 ? Math.min(...freshnessCandidates) : null;
995
- if (freshestCacheAgeMs != null && freshestCacheAgeMs <= COLD_CACHE_FRESHNESS_MS) {
996
- const freshnessSource = cacheAgeFromFileMs != null && cacheAgeFromMetadataMs != null
997
- ? 'file+metadata'
998
- : cacheAgeFromFileMs != null
999
- ? 'file'
1000
- : 'metadata';
1001
- return;
1005
+ // Skip freshness check when cache was just invalidated (VS metadata changed
1006
+ // on the server) — the cold cache file is stale even if recently written.
1007
+ if (!invalidated) {
1008
+ const cacheFilePath = getCacheFilePath(CACHE_VS_DIR, vs.url, vs.version || null, paramsKey);
1009
+ const cacheAgeFromFileMs = getColdCacheAgeMs(cacheFilePath);
1010
+ const persistedCache = this.backgroundExpansionCache.get(cacheKey);
1011
+ const cacheAgeFromMetadataMs = Number.isFinite(persistedCache?.createdAt)
1012
+ ? Math.max(0, Date.now() - persistedCache.createdAt)
1013
+ : null;
1014
+
1015
+ // Treat cache as fresh when either file mtime or persisted timestamp is recent.
1016
+ const freshnessCandidates = [cacheAgeFromFileMs, cacheAgeFromMetadataMs].filter(age => age != null);
1017
+ const freshestCacheAgeMs = freshnessCandidates.length > 0 ? Math.min(...freshnessCandidates) : null;
1018
+ if (freshestCacheAgeMs != null && freshestCacheAgeMs <= COLD_CACHE_FRESHNESS_MS) {
1019
+ return;
1020
+ }
1002
1021
  }
1003
1022
 
1004
1023
  const jobKey = `vs:${cacheKey}`;
@@ -1121,7 +1140,11 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
1121
1140
  if (!systemConcepts.has(entry.system)) {
1122
1141
  systemConcepts.set(entry.system, []);
1123
1142
  }
1124
- systemConcepts.get(entry.system).push(entry.code);
1143
+ const concept = { code: entry.code };
1144
+ if (Array.isArray(entry.designation) && entry.designation.length > 0) {
1145
+ concept.designation = entry.designation;
1146
+ }
1147
+ systemConcepts.get(entry.system).push(concept);
1125
1148
  totalCount++;
1126
1149
  }
1127
1150
  if (progressState) {
@@ -1138,9 +1161,9 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
1138
1161
  }
1139
1162
 
1140
1163
  return {
1141
- include: Array.from(systemConcepts.entries()).map(([system, codes]) => ({
1164
+ include: Array.from(systemConcepts.entries()).map(([system, concepts]) => ({
1142
1165
  system,
1143
- concept: codes.map(code => ({ code }))
1166
+ concept: concepts
1144
1167
  }))
1145
1168
  };
1146
1169
  }
@@ -18,11 +18,11 @@ sources:
18
18
  - unii:unii_20240622.db
19
19
  - snomed:sct_intl_20240201.cache
20
20
  - snomed!:sct_intl_20250201.cache
21
- - snomed:sct_se_20231130.cache
22
- - snomed:sct_au_20230731.cache
23
- - snomed:sct_be_20231115.cache
21
+ # - snomed:sct_se_20231130.cache
22
+ # - snomed:sct_au_20230731.cache
23
+ # - snomed:sct_be_20231115.cache
24
24
  - snomed:sct_ch_20230607.cache
25
- - snomed:sct_dk_20250930.cache
25
+ # - snomed:sct_dk_20250930.cache
26
26
  - snomed:sct_ips_20241216.cache
27
27
  - snomed:sct_nl_20240930.cache
28
28
  - snomed:sct_uk_20230412.cache