fhirsmith 0.7.1 → 0.7.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/tx/ocl/vs-ocl.cjs CHANGED
@@ -15,20 +15,19 @@ const { computeValueSetExpansionFingerprint } = require('./fingerprint/fingerpri
15
15
  const { ensureTxParametersHashIncludesFilter, patchValueSetExpandWholeSystemForOcl } = require('./shared/patches');
16
16
 
17
17
  ensureTxParametersHashIncludesFilter(TxParameters);
18
- patchValueSetExpandWholeSystemForOcl();
18
+ //patchValueSetExpandWholeSystemForOcl();
19
19
 
20
20
  function normalizeCanonicalSystem(system) {
21
21
  if (typeof system !== 'string') {
22
22
  return system;
23
23
  }
24
24
 
25
- const trimmed = system.trim();
25
+ let trimmed = system.trim();
26
26
  if (!trimmed) {
27
27
  return trimmed;
28
28
  }
29
29
 
30
- // Treat canonical URLs with and without trailing slash as equivalent.
31
- return trimmed.replace(/\/+$/, '');
30
+ return trimmed;
32
31
  }
33
32
 
34
33
  class OCLValueSetProvider extends AbstractValueSetProvider {
@@ -94,10 +93,42 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
94
93
  dependencyChecksums: cached.dependencyChecksums || {},
95
94
  createdAt: Number.isFinite(createdAt) ? createdAt : null
96
95
  });
97
-
96
+ // Instancia ValueSet para garantir jsonObj
97
+ // Reconstrói compose.include se não existir
98
+ let compose = cached.expansion?.compose;
99
+ if (!compose || !Array.isArray(compose.include)) {
100
+ // Reconstrói a partir dos sistemas/códigos em expansion.contains
101
+ const systemConcepts = new Map();
102
+ if (Array.isArray(cached.expansion?.contains)) {
103
+ for (const entry of cached.expansion.contains) {
104
+ if (!entry.system || !entry.code) continue;
105
+ if (!systemConcepts.has(entry.system)) {
106
+ systemConcepts.set(entry.system, []);
107
+ }
108
+ systemConcepts.get(entry.system).push({ code: entry.code });
109
+ }
110
+ }
111
+ compose = { include: Array.from(systemConcepts.entries()).map(([system, concepts]) => ({ system, concept: concepts })) };
112
+ }
113
+ const valueSetObj = new ValueSet({
114
+ resourceType: 'ValueSet',
115
+ url: cached.canonicalUrl,
116
+ version: cached.version || null,
117
+ expansion: cached.expansion,
118
+ compose,
119
+ id: cached.canonicalUrl // ou outro identificador se necessário
120
+ }, 'R5');
121
+ this.#applyCachedExpansion(valueSetObj, paramsKey);
122
+ // Indexa o ValueSet restaurado para torná-lo disponível via fetchValueSet
123
+ this.valueSetMap.set(valueSetObj.url, valueSetObj);
124
+ if (valueSetObj.version) {
125
+ this.valueSetMap.set(`${valueSetObj.url}|${valueSetObj.version}`, valueSetObj);
126
+ }
127
+ this.valueSetMap.set(valueSetObj.id, valueSetObj);
128
+ this._idMap.set(valueSetObj.id, valueSetObj);
98
129
  this.valueSetFingerprints.set(cacheKey, cached.fingerprint || null);
99
130
  loadedCount++;
100
- console.log(`[OCL-ValueSet] Loaded ValueSet from cold cache: ${cached.canonicalUrl}`);
131
+ console.log(`[OCL-ValueSet] Loaded ValueSet from cold cache into memory: ${cached.canonicalUrl}`);
101
132
  } catch (error) {
102
133
  console.error(`[OCL-ValueSet] Failed to load cold cache file ${file}:`, error.message);
103
134
  }
@@ -221,9 +252,10 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
221
252
  let key = `${url}|${version}`;
222
253
  if (this.valueSetMap.has(key)) {
223
254
  const vs = this.valueSetMap.get(key);
224
- await this.#ensureComposeIncludes(vs);
255
+ // await this.#ensureComposeIncludes(vs);
225
256
  this.#clearInlineExpansion(vs);
226
- this.#scheduleBackgroundExpansion(vs, { reason: 'fetch-valueset' });
257
+ console.log(`[OCL-ValueSet] fetchValueSet cache hit for ${url} (version: ${version || 'none'})`);
258
+ this.#scheduleBackgroundExpansion(vs, { reason: 'fetch-valueset', userRequested: true });
227
259
  return vs;
228
260
  }
229
261
 
@@ -233,7 +265,7 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
233
265
  key = `${url}|${majorMinor}`;
234
266
  if (this.valueSetMap.has(key)) {
235
267
  const vs = this.valueSetMap.get(key);
236
- await this.#ensureComposeIncludes(vs);
268
+ // await this.#ensureComposeIncludes(vs);
237
269
  this.#clearInlineExpansion(vs);
238
270
  this.#scheduleBackgroundExpansion(vs, { reason: 'fetch-valueset-mm' });
239
271
  return vs;
@@ -243,7 +275,7 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
243
275
 
244
276
  if (this.valueSetMap.has(url)) {
245
277
  const vs = this.valueSetMap.get(url);
246
- await this.#ensureComposeIncludes(vs);
278
+ // await this.#ensureComposeIncludes(vs);
247
279
  this.#clearInlineExpansion(vs);
248
280
  this.#scheduleBackgroundExpansion(vs, { reason: 'fetch-valueset-url' });
249
281
  return vs;
@@ -251,7 +283,7 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
251
283
 
252
284
  const resolved = await this.#resolveValueSetByCanonical(url, version);
253
285
  if (resolved) {
254
- await this.#ensureComposeIncludes(resolved);
286
+ // await this.#ensureComposeIncludes(resolved);
255
287
  this.#clearInlineExpansion(resolved);
256
288
  this.#scheduleBackgroundExpansion(resolved, { reason: 'fetch-valueset-resolved' });
257
289
  return resolved;
@@ -262,7 +294,7 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
262
294
  key = `${url}|${version}`;
263
295
  if (this.valueSetMap.has(key)) {
264
296
  const vs = this.valueSetMap.get(key);
265
- await this.#ensureComposeIncludes(vs);
297
+ // await this.#ensureComposeIncludes(vs);
266
298
  this.#clearInlineExpansion(vs);
267
299
  this.#scheduleBackgroundExpansion(vs, { reason: 'fetch-valueset-init' });
268
300
  return vs;
@@ -274,7 +306,7 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
274
306
  key = `${url}|${majorMinor}`;
275
307
  if (this.valueSetMap.has(key)) {
276
308
  const vs = this.valueSetMap.get(key);
277
- await this.#ensureComposeIncludes(vs);
309
+ // await this.#ensureComposeIncludes(vs);
278
310
  this.#clearInlineExpansion(vs);
279
311
  this.#scheduleBackgroundExpansion(vs, { reason: 'fetch-valueset-init-mm' });
280
312
  return vs;
@@ -284,7 +316,7 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
284
316
 
285
317
  if (this.valueSetMap.has(url)) {
286
318
  const vs = this.valueSetMap.get(url);
287
- await this.#ensureComposeIncludes(vs);
319
+ // await this.#ensureComposeIncludes(vs);
288
320
  this.#clearInlineExpansion(vs);
289
321
  this.#scheduleBackgroundExpansion(vs, { reason: 'fetch-valueset-init-url' });
290
322
  return vs;
@@ -296,7 +328,7 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
296
328
  async fetchValueSetById(id) {
297
329
  const local = this.#getLocalValueSetById(id);
298
330
  if (local) {
299
- await this.#ensureComposeIncludes(local);
331
+ // await this.#ensureComposeIncludes(local);
300
332
  this.#clearInlineExpansion(local);
301
333
  this.#scheduleBackgroundExpansion(local, { reason: 'fetch-valueset-by-id' });
302
334
  return local;
@@ -305,7 +337,7 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
305
337
  await this.initialize();
306
338
 
307
339
  const vs = this.#getLocalValueSetById(id);
308
- await this.#ensureComposeIncludes(vs);
340
+ // await this.#ensureComposeIncludes(vs);
309
341
  this.#clearInlineExpansion(vs);
310
342
  this.#scheduleBackgroundExpansion(vs, { reason: 'fetch-valueset-by-id-init' });
311
343
  return vs;
@@ -370,17 +402,15 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
370
402
  || this._idMap.get(vs.id)
371
403
  || null;
372
404
 
373
- // Preserve hydrated cold-cache expansions on first index; invalidate only on replacement.
374
- if (existing && existing !== vs) {
375
- this.#invalidateExpansionCache(vs);
376
- }
377
-
378
- this.valueSetMap.set(vs.url, vs);
379
- if (vs.version) {
380
- this.valueSetMap.set(`${vs.url}|${vs.version}`, vs);
405
+ // indexa se não existe ou se for o mesmo objeto
406
+ if (!existing || existing === vs) {
407
+ this.valueSetMap.set(vs.url, vs);
408
+ if (vs.version) {
409
+ this.valueSetMap.set(`${vs.url}|${vs.version}`, vs);
410
+ }
411
+ this.valueSetMap.set(vs.id, vs);
412
+ this._idMap.set(vs.id, vs);
381
413
  }
382
- this.valueSetMap.set(vs.id, vs);
383
- this._idMap.set(vs.id, vs);
384
414
  }
385
415
 
386
416
  #toValueSet(collection) {
@@ -415,11 +445,11 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
415
445
  json.meta = { lastUpdated };
416
446
  }
417
447
 
418
- if (preferredSource) {
419
- json.compose = {
420
- include: [{ system: preferredSource }]
421
- };
422
- }
448
+ // if (preferredSource) {
449
+ // json.compose = {
450
+ // include: [{ system: preferredSource }]
451
+ // };
452
+ // }
423
453
 
424
454
  const conceptsUrl = this.#normalizePath(
425
455
  collection.concepts_url || collection.conceptsUrl || this.#buildCollectionConceptsPath(collection)
@@ -484,7 +514,15 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
484
514
  if (pathValue.startsWith('http://') || pathValue.startsWith('https://')) {
485
515
  return pathValue;
486
516
  }
487
- return `${this.baseUrl}${pathValue.startsWith('/') ? '' : '/'}${pathValue}`;
517
+ // Remove extra slashes and normalize full URL
518
+ let base = this.baseUrl.replace(/\/+$/, '');
519
+ let path = pathValue.replace(/^\/+/, '');
520
+ let url = `${base}/${path}`;
521
+ // Remove all duplicate slashes except after protocol
522
+ url = url.replace(/([^:])\/+/g, '$1/');
523
+ // Remove trailing slashes
524
+ url = url.replace(/\/+$/, '');
525
+ return url;
488
526
  }
489
527
 
490
528
  #buildCollectionConceptsPath(collection) {
@@ -868,7 +906,9 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
868
906
  if (!base) {
869
907
  return null;
870
908
  }
871
- return `${base}|${paramsKey || 'default'}`;
909
+ const crypto = require('crypto');
910
+ const hash = crypto.createHash('sha256').update(`${base}|${paramsKey || 'default'}`).digest('hex');
911
+ return hash;
872
912
  }
873
913
 
874
914
  #invalidateExpansionCache(vs) {
@@ -986,7 +1026,8 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
986
1026
  const meta = this.#getCollectionMeta(vs);
987
1027
  queuedJobSize = await this.#fetchConceptCountFromHeaders(meta?.conceptsUrl || null);
988
1028
  return queuedJobSize;
989
- }
1029
+ },
1030
+ userRequested: !!options.userRequested
990
1031
  }
991
1032
  );
992
1033
  }
@@ -1071,10 +1112,10 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
1071
1112
  }
1072
1113
 
1073
1114
  const contains = [];
1074
- let offset = 0;
1075
-
1076
- // Pull all concepts in fixed-size pages until exhausted.
1077
- // eslint-disable-next-line no-constant-condition
1115
+ let offset = 0; // Moved this line up
1116
+ // Agrupa conceitos por system
1117
+ const systemConcepts = new Map();
1118
+ // Removed duplicate offset declaration
1078
1119
  while (true) {
1079
1120
  const batch = await this.#fetchCollectionConcepts(meta, {
1080
1121
  count: CONCEPT_PAGE_SIZE,
@@ -1083,17 +1124,14 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
1083
1124
  filter: null,
1084
1125
  languageCodes: []
1085
1126
  });
1086
-
1087
1127
  const entries = Array.isArray(batch?.contains) ? batch.contains : [];
1088
1128
  if (entries.length === 0) {
1089
1129
  break;
1090
1130
  }
1091
-
1092
1131
  for (const entry of entries) {
1093
1132
  if (!entry?.system || !entry?.code) {
1094
1133
  continue;
1095
1134
  }
1096
-
1097
1135
  const out = {
1098
1136
  system: entry.system,
1099
1137
  code: entry.code
@@ -1116,18 +1154,28 @@ class OCLValueSetProvider extends AbstractValueSetProvider {
1116
1154
  }));
1117
1155
  }
1118
1156
  contains.push(out);
1157
+ // Agrupa por system
1158
+ if (!systemConcepts.has(entry.system)) {
1159
+ systemConcepts.set(entry.system, []);
1160
+ }
1161
+ systemConcepts.get(entry.system).push(entry.code);
1119
1162
  }
1120
-
1121
1163
  if (progressState) {
1122
1164
  progressState.processed = contains.length;
1123
1165
  }
1124
-
1125
1166
  if (entries.length < CONCEPT_PAGE_SIZE) {
1126
1167
  break;
1127
1168
  }
1128
1169
  offset += entries.length;
1129
1170
  }
1130
-
1171
+ // Popular compose.include para cada system
1172
+ if (!vs.jsonObj.compose) {
1173
+ vs.jsonObj.compose = { include: [] };
1174
+ }
1175
+ vs.jsonObj.compose.include = Array.from(systemConcepts.entries()).map(([system, codes]) => ({
1176
+ system,
1177
+ concept: codes.map(code => ({ code }))
1178
+ }));
1131
1179
  return {
1132
1180
  timestamp: new Date().toISOString(),
1133
1181
  identifier: `urn:uuid:${crypto.randomUUID()}`,
@@ -549,6 +549,7 @@ class ValueSetExpander {
549
549
  }
550
550
 
551
551
  async importValueSet(vs, expansion, imports, offset) {
552
+ let count = 0;
552
553
  this.canBeHierarchy = false;
553
554
  for (let p of vs.expansion.parameter || []) {
554
555
  let vn = getValueName(p);
@@ -559,14 +560,17 @@ class ValueSetExpander {
559
560
 
560
561
  for (const c of vs.expansion.contains || []) {
561
562
  this.worker.deadCheck('importValueSet');
562
- await this.importValueSetItem(null, c, imports, offset);
563
+ count += await this.importValueSetItem(null, c, imports, offset);
563
564
  }
565
+ return count;
564
566
  }
565
567
 
566
568
  async importValueSetItem(p, c, imports, offset) {
569
+ let count = 0;
567
570
  this.worker.deadCheck('importValueSetItem');
568
571
  const s = this.keyC(c);
569
- if (this.passesImports(imports, c.system, c.code, offset) && !this.map.has(s)) {
572
+ if (this.passesImports(imports, c.system, c.code, offset) && !this.map.has(s) && !this.isExcluded(c.system, c.version, c.code)) {
573
+ count++;
570
574
  this.fullList.push(c);
571
575
  if (p != null) {
572
576
  if (!p.contains) {p.contains = [] }
@@ -578,8 +582,9 @@ class ValueSetExpander {
578
582
  }
579
583
  for (const cc of c.contains || []) {
580
584
  this.worker.deadCheck('importValueSetItem');
581
- await this.importValueSetItem(c, cc, imports, offset);
585
+ count += await this.importValueSetItem(c, cc, imports, offset);
582
586
  }
587
+ return count;
583
588
  }
584
589
 
585
590
  excludeValueSet(vs, expansion, imports, offset) {
@@ -636,8 +641,8 @@ class ValueSetExpander {
636
641
  throw new Issue('error', 'business-rule', null, null, 'The code system definition for ' + cset.system + ' defines a supplement, so this expansion cannot be performed', 'invalid');
637
642
  } else {
638
643
  this.addParamUri(exp, cs.contentMode(), cs.system() + '|' + cs.version());
639
- Extensions.addString(exp, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed",
640
- "This extension is based on a fragment of the code system " + cset.system);
644
+ Extensions.addBoolean(exp, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed", true);
645
+ Extensions.addString(exp, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed-reason","This extension is based on a fragment of the code system " + cset.system);
641
646
  }
642
647
  }
643
648
 
@@ -647,7 +652,8 @@ class ValueSetExpander {
647
652
  } else if (filter.isNull) {
648
653
  if (cs.isNotClosed()) {
649
654
  if (cs.specialEnumeration()) {
650
- Extensions.addString(exp, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed", 'The code System "' + cs.system() + " has a grammar and so has infinite members. This extension is based on " + cs.specialEnumeration());
655
+ Extensions.addBoolean(exp, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed", true);
656
+ Extensions.addString(exp, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed-reason", 'The code System "' + cs.system() + " has a grammar and so has infinite members. This extension is based on " + cs.specialEnumeration());
651
657
  } else {
652
658
  throw new Issue("error", "too-costly", null, null, 'The code System "' + cs.system() + '" has a grammar, and cannot be enumerated directly', null, 422).withDiagnostics(this.worker.opContext.diagnostics());
653
659
  }
@@ -692,7 +698,7 @@ class ValueSetExpander {
692
698
  this.addParamUri(expansion, 'used-valueset', this.worker.makeVurl(ivs.valueSet));
693
699
  valueSets.push(ivs);
694
700
  }
695
- await this.importValueSet(valueSets[0].valueSet, expansion, valueSets, 1);
701
+ this.addToTotal(await this.importValueSet(valueSets[0].valueSet, expansion, valueSets, 1));
696
702
  } else {
697
703
  const filters = [];
698
704
  const prep = null;
@@ -721,14 +727,16 @@ class ValueSetExpander {
721
727
  if (cs.specialEnumeration() && filters.length === 0) {
722
728
  this.worker.opContext.log('import special value set ' + cs.specialEnumeration());
723
729
  const base = await this.expandValueSet(cs.specialEnumeration(), '', filter, notClosed);
724
- Extensions.addString(expansion, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed", 'The code System "' + cs.system() + " has a grammar and so has infinite members. This extension is based on " + cs.specialEnumeration());
730
+ Extensions.addBoolean(expansion, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed", true);
731
+ Extensions.addString(expansion, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed-reason", 'The code System "' + cs.system() + " has a grammar and so has infinite members. This extension is based on " + cs.specialEnumeration());
725
732
  await this.importValueSet(base, expansion, valueSets, 0);
726
733
  notClosed.value = true;
727
734
  } else if (filter.isNull) {
728
735
  this.worker.opContext.log('add whole code system');
729
736
  if (cs.isNotClosed()) {
730
737
  if (cs.specialEnumeration()) {
731
- Extensions.addString(expansion, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed", 'The code System "' + cs.system() + " has a grammar and so has infinite members. This extension is based on " + cs.specialEnumeration());
738
+ Extensions.addBoolean(expansion, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed", true);
739
+ Extensions.addString(expansion, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed-reason", 'The code System "' + cs.system() + " has a grammar and so has infinite members. This extension is based on " + cs.specialEnumeration());
732
740
  } else {
733
741
  throw new Issue("error", "too-costly", null, null, 'The code System "' + cs.system() + '" has a grammar, and cannot be enumerated directly', null, 422).withDiagnostics(this.worker.opContext.diagnostics());
734
742
  }
@@ -815,7 +823,8 @@ class ValueSetExpander {
815
823
  }
816
824
 
817
825
  if (cs.specialEnumeration()) {
818
- Extensions.addString(expansion, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed", 'The code System "' + cs.system() + " has a grammar and so has infinite members. This extension is based on " + cs.specialEnumeration());
826
+ Extensions.addBoolean(expansion, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed", true);
827
+ Extensions.addString(expansion, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed-reason", 'The code System "' + cs.system() + " has a grammar and so has infinite members. This extension is based on " + cs.specialEnumeration());
819
828
  notClosed.value = true;
820
829
  }
821
830
 
@@ -942,7 +951,8 @@ class ValueSetExpander {
942
951
  } else {
943
952
  if (cs.isNotClosed(filter)) {
944
953
  if (cs.specialEnumeration()) {
945
- Extensions.addString(expansion, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed", 'The code System "' + cs.system() + " has a grammar and so has infinite members. This extension is based on " + cs.specialEnumeration());
954
+ Extensions.addBoolean(expansion, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed", true);
955
+ Extensions.addString(expansion, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed-reason", 'The code System "' + cs.system() + " has a grammar and so has infinite members. This extension is based on " + cs.specialEnumeration());
946
956
  } else {
947
957
  throw new Issue("error", "too-costly", null, null, 'The code System "' + cs.system() + '" has a grammar, and cannot be enumerated directly', null, 422).withDiagnostics(this.worker.opContext.diagnostics());
948
958
  }
@@ -991,7 +1001,8 @@ class ValueSetExpander {
991
1001
 
992
1002
  if (cs.specialEnumeration()) {
993
1003
  await cs.specialFilter(prep, true);
994
- Extensions.addString(expansion, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed", 'The code System "' + cs.system() + " has a grammar and so has infinite members. This extension is based on " + cs.specialEnumeration());
1004
+ Extensions.addBoolean(expansion, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed", true);
1005
+ Extensions.addString(expansion, "http://hl7.org/fhir/StructureDefinition/valueset-unclosed-reason", 'The code System "' + cs.system() + " has a grammar and so has infinite members. This extension is based on " + cs.specialEnumeration());
995
1006
  }
996
1007
 
997
1008
  for (let fc of cset.filter) {
@@ -32,15 +32,18 @@ class SearchWorker extends TerminologyWorker {
32
32
 
33
33
  // Allowed search parameters
34
34
  static ALLOWED_PARAMS = [
35
- '_offset', '_count', '_elements', '_sort', '_summary', '_total',
35
+ '_offset', '_count', '_elements', '_sort', '_summary', '_total', '_format',
36
36
  'url', 'version', 'content-mode', 'date', 'description',
37
37
  'supplements', 'identifier', 'jurisdiction', 'name',
38
38
  'publisher', 'status', 'system', 'title', 'text'
39
39
  ];
40
40
 
41
- // Summary elements for _summary=true (common metadata fields)
42
- static SUMMARY_ELEMENTS = ['resourceType', 'id', 'meta', 'url', 'version',
43
- 'name', 'title', 'status', 'date', 'publisher', 'description'];
41
+ // Summary elements for _summary=true (marked elements per resource type)
42
+ static SUMMARY_ELEMENTS = {
43
+ CodeSystem: ['meta', 'url', 'version', 'name', 'title', 'status', 'experimental', 'date', 'publisher', 'description', 'jurisdiction', 'content'],
44
+ ValueSet: ['meta', 'url', 'version', 'name', 'title', 'status', 'experimental', 'date', 'publisher', 'description', 'jurisdiction'],
45
+ ConceptMap: ['meta', 'url', 'version', 'name', 'title', 'status', 'experimental', 'date', 'publisher', 'description', 'jurisdiction']
46
+ };
44
47
 
45
48
  // Sortable fields
46
49
  static SORT_FIELDS = ['id', 'url', 'version', 'date', 'name', 'vurl'];
@@ -67,7 +70,7 @@ class SearchWorker extends TerminologyWorker {
67
70
  let elements;
68
71
  switch (summary) {
69
72
  case 'true':
70
- elements = SearchWorker.SUMMARY_ELEMENTS;
73
+ elements = SearchWorker.SUMMARY_ELEMENTS[resourceType] || [];
71
74
  break;
72
75
  case 'text':
73
76
  elements = ['resourceType', 'id', 'meta', 'text'];
@@ -110,7 +113,7 @@ class SearchWorker extends TerminologyWorker {
110
113
  const bundle = this.buildSearchBundle(
111
114
  req, resourceType, matches, offset, count, elements, summary, totalMode
112
115
  );
113
- req.logInfo = summary === 'count' ? `count: ${bundle.total}` : `${bundle.entry.length} matches`;
116
+ req.logInfo = `${bundle.entry ? bundle.entry.length : 0} matches`;
114
117
  return res.json(bundle);
115
118
 
116
119
  } catch (error) {
@@ -336,15 +339,15 @@ class SearchWorker extends TerminologyWorker {
336
339
  /**
337
340
  * Build a FHIR search Bundle with pagination
338
341
  */
339
- buildSearchBundle(req, resourceType, allMatches, offset, count, elements, summary = 'false', totalMode = 'accurate') {
340
- const total = allMatches.length;
342
+ buildSearchBundle(req, resourceType, allMatches, offset, count, elements, summary, totalParam) {
343
+ const totalCount = allMatches.length;
341
344
 
342
- // For _summary=count, return just the count
345
+ // Handle _summary=count - only return total, no entries
343
346
  if (summary === 'count') {
344
347
  return {
345
348
  resourceType: 'Bundle',
346
349
  type: 'searchset',
347
- total: total
350
+ total: totalCount
348
351
  };
349
352
  }
350
353
 
@@ -396,7 +399,7 @@ class SearchWorker extends TerminologyWorker {
396
399
  }
397
400
 
398
401
  // Next link (if more results)
399
- if (offset + count < total) {
402
+ if (offset + count < totalCount) {
400
403
  const nextParams = new URLSearchParams(searchParams);
401
404
  nextParams.set('_offset', offset + count);
402
405
  links.push({
@@ -406,7 +409,7 @@ class SearchWorker extends TerminologyWorker {
406
409
  }
407
410
 
408
411
  // Last link
409
- const lastOffset = Math.max(0, Math.floor((total - 1) / count) * count);
412
+ const lastOffset = Math.max(0, Math.floor((totalCount - 1) / count) * count);
410
413
  const lastParams = new URLSearchParams(searchParams);
411
414
  lastParams.set('_offset', lastOffset);
412
415
  links.push({
@@ -416,7 +419,7 @@ class SearchWorker extends TerminologyWorker {
416
419
 
417
420
  // Build entries
418
421
  const entries = pageResults.map(resource => {
419
- // Apply _elements filter if specified
422
+ // Apply _elements or _summary filter if specified
420
423
  let filteredResource = resource;
421
424
  if (elements) {
422
425
  filteredResource = this.filterElements(resource, elements);
@@ -437,8 +440,9 @@ class SearchWorker extends TerminologyWorker {
437
440
  link: links,
438
441
  entry: entries
439
442
  };
440
- if (totalMode !== 'none') {
441
- bundle.total = total;
443
+ // Add total unless _total=none
444
+ if (totalParam !== 'none') {
445
+ bundle.total = totalCount;
442
446
  }
443
447
  return bundle;
444
448
  }
@@ -459,6 +463,13 @@ class SearchWorker extends TerminologyWorker {
459
463
  }
460
464
  }
461
465
 
466
+ // Mark as SUBSETTED per FHIR spec
467
+ filtered.meta = filtered.meta ? { ...filtered.meta } : {};
468
+ filtered.meta.tag = [
469
+ ...(filtered.meta.tag || []),
470
+ { system: 'http://terminology.hl7.org/CodeSystem/v3-ObservationValue', code: 'SUBSETTED' }
471
+ ];
472
+
462
473
  return filtered;
463
474
  }
464
475
  }