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/CHANGELOG.md +42 -2
- package/config-template.json +16 -0
- package/extension-tracker/extension-tracker-template.html +124 -0
- package/extension-tracker/extension-tracker.js +697 -0
- package/extension-tracker/readme.md +63 -0
- package/folder/folder.js +305 -0
- package/folder/readme.md +57 -0
- package/library/html-server.js +8 -2
- package/package.json +4 -2
- package/packages/packages.js +8 -8
- package/publisher/publisher.js +104 -13
- package/server.js +58 -4
- package/tx/cs/cs-snomed.js +13 -7
- package/tx/library/extensions.js +6 -2
- package/tx/library/renderer.js +1 -1
- package/tx/ocl/cache/cache-paths.cjs +4 -5
- package/tx/ocl/cm-ocl.cjs +4 -1
- package/tx/ocl/cs-ocl.cjs +9 -9
- package/tx/ocl/jobs/background-queue.cjs +11 -5
- package/tx/ocl/shared/patches.cjs +37 -0
- package/tx/ocl/vs-ocl.cjs +92 -44
- package/tx/workers/expand.js +23 -12
- package/tx/workers/search.js +26 -15
- package/xig/xig.js +59 -8
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
|
-
|
|
25
|
+
let trimmed = system.trim();
|
|
26
26
|
if (!trimmed) {
|
|
27
27
|
return trimmed;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
374
|
-
if (existing
|
|
375
|
-
this
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
this.
|
|
405
|
+
// Só 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
|
-
|
|
420
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1077
|
-
//
|
|
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()}`,
|
package/tx/workers/expand.js
CHANGED
|
@@ -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.
|
|
640
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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) {
|
package/tx/workers/search.js
CHANGED
|
@@ -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 (
|
|
42
|
-
static SUMMARY_ELEMENTS =
|
|
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 =
|
|
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
|
|
340
|
-
const
|
|
342
|
+
buildSearchBundle(req, resourceType, allMatches, offset, count, elements, summary, totalParam) {
|
|
343
|
+
const totalCount = allMatches.length;
|
|
341
344
|
|
|
342
|
-
//
|
|
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:
|
|
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 <
|
|
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((
|
|
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
|
-
|
|
441
|
-
|
|
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
|
}
|