fhirsmith 0.7.6 → 0.8.0
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 +26 -0
- package/library/languages.js +10 -0
- package/package.json +1 -1
- package/packages/package-crawler.js +2 -2
- package/publisher/publisher.js +1 -1
- package/registry/registry.js +2 -2
- package/root-bare-template.html +1 -2
- package/server.js +100 -70
- package/stats.js +37 -6
- package/tx/cs/cs-loinc.js +14 -2
- package/tx/cs/cs-rxnorm.js +14 -10
- package/tx/cs/cs-snomed.js +166 -5
- package/tx/html/dash-metrics.liquid +147 -0
- package/tx/importers/import-rxnorm.module.js +4 -30
- package/tx/library/canonical-resource.js +8 -0
- package/tx/library/conceptmap.js +3 -1
- package/tx/library/designations.js +4 -8
- package/tx/library/renderer.js +9 -9
- package/tx/ocl/cm-ocl.cjs +185 -65
- package/tx/ocl/cs-ocl.cjs +69 -50
- package/tx/ocl/jobs/background-queue.cjs +0 -8
- package/tx/ocl/mappers/concept-mapper.cjs +13 -3
- package/tx/ocl/shared/patches.cjs +1 -0
- package/tx/ocl/vs-ocl.cjs +137 -157
- package/tx/operation-context.js +3 -3
- package/tx/provider.js +2 -2
- package/tx/sct/structures.js +5 -0
- package/tx/tx.fhir.org.yml +1 -1
- package/tx/vs/vs-database.js +107 -23
- package/tx/vs/vs-vsac.js +66 -19
- package/tx/workers/search.js +2 -1
- package/tx/workers/translate.js +39 -14
- package/tx/workers/validate.js +3 -3
- package/xig/xig.js +171 -9
package/tx/ocl/cm-ocl.cjs
CHANGED
|
@@ -59,15 +59,19 @@ class OCLConceptMapProvider extends AbstractConceptMapProvider {
|
|
|
59
59
|
return await this.fetchConceptMapById(mappingId);
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
62
|
+
try {
|
|
63
|
+
const mappings = await this.#searchMappings({ from_source_url: url }, this.maxSearchPages);
|
|
64
|
+
for (const mapping of mappings) {
|
|
65
|
+
const cm = this.#toConceptMap(mapping);
|
|
66
|
+
if (cm) {
|
|
67
|
+
this.#indexConceptMap(cm);
|
|
68
|
+
if (cm.url === url && (!version || cm.version === version)) {
|
|
69
|
+
return cm;
|
|
70
|
+
}
|
|
69
71
|
}
|
|
70
72
|
}
|
|
73
|
+
} catch (_err) {
|
|
74
|
+
// OCL API unreachable or returned error — treat as not found
|
|
71
75
|
}
|
|
72
76
|
|
|
73
77
|
return null;
|
|
@@ -87,45 +91,194 @@ class OCLConceptMapProvider extends AbstractConceptMapProvider {
|
|
|
87
91
|
return this._idMap.get(rawId);
|
|
88
92
|
}
|
|
89
93
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
94
|
+
try {
|
|
95
|
+
const response = await this.httpClient.get(`/mappings/${encodeURIComponent(rawId)}/`);
|
|
96
|
+
const cm = this.#toConceptMap(response.data);
|
|
97
|
+
if (!cm) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
this.#indexConceptMap(cm);
|
|
101
|
+
return cm;
|
|
102
|
+
} catch (_err) {
|
|
93
103
|
return null;
|
|
94
104
|
}
|
|
95
|
-
this.#indexConceptMap(cm);
|
|
96
|
-
return cm;
|
|
97
105
|
}
|
|
98
106
|
|
|
99
|
-
// eslint-disable-next-line no-unused-vars
|
|
100
107
|
async searchConceptMaps(searchParams, _elements) {
|
|
101
108
|
this._validateSearchParams(searchParams);
|
|
102
109
|
|
|
103
|
-
const params = Object.fromEntries(
|
|
104
|
-
|
|
110
|
+
const params = Object.fromEntries(
|
|
111
|
+
searchParams.map(({ name, value }) => [name, String(value).toLowerCase()])
|
|
112
|
+
);
|
|
113
|
+
const sourceSystem = params['source-system'] || params.source || null;
|
|
114
|
+
const targetSystem = params['target-system'] || params.target || null;
|
|
105
115
|
|
|
106
|
-
|
|
107
|
-
|
|
116
|
+
// Without a source or target filter the search would have to fetch every
|
|
117
|
+
// mapping in the organisation — too expensive. Return empty so the
|
|
118
|
+
// Package providers can still answer generic ConceptMap listings.
|
|
119
|
+
if (!sourceSystem && !targetSystem) {
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
const allMappings = await this.#collectMappingsForSearch(sourceSystem, targetSystem);
|
|
125
|
+
return this.#aggregateMappingsToConceptMaps(allMappings);
|
|
126
|
+
} catch (_err) {
|
|
127
|
+
return [];
|
|
108
128
|
}
|
|
109
|
-
|
|
110
|
-
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async #collectMappingsForSearch(sourceSystem, targetSystem) {
|
|
132
|
+
const systemUrl = sourceSystem || targetSystem;
|
|
133
|
+
const candidates = await this.#candidateSourceUrls(systemUrl);
|
|
134
|
+
const sourcePaths = candidates.filter(s => String(s || '').startsWith('/orgs/'));
|
|
135
|
+
|
|
136
|
+
if (sourcePaths.length === 0) {
|
|
137
|
+
return [];
|
|
111
138
|
}
|
|
112
139
|
|
|
113
|
-
const
|
|
114
|
-
const
|
|
140
|
+
const allMappings = [];
|
|
141
|
+
for (const sourcePath of sourcePaths) {
|
|
142
|
+
const normalizedPath = this.#normalizeSourcePath(sourcePath);
|
|
143
|
+
let concepts;
|
|
144
|
+
try {
|
|
145
|
+
concepts = await this.#fetchAllPages(
|
|
146
|
+
`${normalizedPath}concepts/`, { limit: PAGE_SIZE }, this.maxSearchPages
|
|
147
|
+
);
|
|
148
|
+
} catch (_err) {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
for (const concept of concepts) {
|
|
153
|
+
const code = concept.id || concept.mnemonic;
|
|
154
|
+
if (!code) {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
const mappings = await this.#fetchAllPages(
|
|
159
|
+
`${normalizedPath}concepts/${encodeURIComponent(code)}/mappings/`,
|
|
160
|
+
{ limit: PAGE_SIZE }, 2
|
|
161
|
+
);
|
|
162
|
+
allMappings.push(...mappings);
|
|
163
|
+
} catch (_err) {
|
|
164
|
+
// concept has no mappings or endpoint inaccessible — skip
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const sourceUrlsToResolve = new Set();
|
|
170
|
+
for (const m of allMappings) {
|
|
171
|
+
const from = m?.from_source_url || m?.fromSourceUrl;
|
|
172
|
+
const to = m?.to_source_url || m?.toSourceUrl;
|
|
173
|
+
if (from) sourceUrlsToResolve.add(from);
|
|
174
|
+
if (to) sourceUrlsToResolve.add(to);
|
|
175
|
+
}
|
|
176
|
+
await this.#ensureCanonicalForSourceUrls(sourceUrlsToResolve);
|
|
177
|
+
|
|
178
|
+
return allMappings;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
#aggregateMappingsToConceptMaps(mappings) {
|
|
182
|
+
const groups = new Map();
|
|
183
|
+
|
|
115
184
|
for (const mapping of mappings) {
|
|
116
|
-
const
|
|
117
|
-
|
|
185
|
+
const fromSource = mapping.from_source_url || mapping.fromSourceUrl || null;
|
|
186
|
+
const toSource = mapping.to_source_url || mapping.toSourceUrl || null;
|
|
187
|
+
const sourceCode = mapping.from_concept_code || mapping.fromConceptCode;
|
|
188
|
+
const targetCode = mapping.to_concept_code || mapping.toConceptCode;
|
|
189
|
+
|
|
190
|
+
if (!fromSource || !toSource || !sourceCode || !targetCode) {
|
|
118
191
|
continue;
|
|
119
192
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
193
|
+
|
|
194
|
+
const sourceCanonical = this.#canonicalForSourceUrl(fromSource) || fromSource;
|
|
195
|
+
const targetCanonical = this.#canonicalForSourceUrl(toSource) || toSource;
|
|
196
|
+
const groupKey = `${this.#norm(sourceCanonical)}|${this.#norm(targetCanonical)}`;
|
|
197
|
+
|
|
198
|
+
if (!groups.has(groupKey)) {
|
|
199
|
+
groups.set(groupKey, {
|
|
200
|
+
sourceCanonical, targetCanonical, elements: new Map(), lastUpdated: null
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const group = groups.get(groupKey);
|
|
205
|
+
const ts = this.#toIsoDate(
|
|
206
|
+
mapping.updated_on || mapping.updatedOn || mapping.updated_at || mapping.updatedAt
|
|
207
|
+
);
|
|
208
|
+
if (ts && (!group.lastUpdated || ts > group.lastUpdated)) {
|
|
209
|
+
group.lastUpdated = ts;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (!group.elements.has(sourceCode)) {
|
|
213
|
+
group.elements.set(sourceCode, {
|
|
214
|
+
code: sourceCode,
|
|
215
|
+
display: mapping.from_concept_name_resolved || mapping.fromConceptNameResolved
|
|
216
|
+
|| mapping.from_concept_name || mapping.fromConceptName || null,
|
|
217
|
+
targets: []
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
group.elements.get(sourceCode).targets.push({
|
|
222
|
+
code: targetCode,
|
|
223
|
+
display: mapping.to_concept_name_resolved || mapping.toConceptNameResolved
|
|
224
|
+
|| mapping.to_concept_name || mapping.toConceptName || null,
|
|
225
|
+
relationship: this.#toRelationship(mapping.map_type || mapping.mapType),
|
|
226
|
+
comment: mapping.comment || null
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const results = [];
|
|
231
|
+
for (const [, group] of groups) {
|
|
232
|
+
const sourceId = this.#lastSegment(group.sourceCanonical);
|
|
233
|
+
const targetId = this.#lastSegment(group.targetCanonical);
|
|
234
|
+
const id = `${sourceId}-to-${targetId}`;
|
|
235
|
+
|
|
236
|
+
const elements = [];
|
|
237
|
+
for (const [, el] of group.elements) {
|
|
238
|
+
elements.push({ code: el.code, display: el.display, target: el.targets });
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const json = {
|
|
242
|
+
resourceType: 'ConceptMap',
|
|
243
|
+
id,
|
|
244
|
+
url: `${this.baseUrl}/ConceptMap/${id}`,
|
|
245
|
+
name: id,
|
|
246
|
+
title: `${sourceId} to ${targetId}`,
|
|
247
|
+
status: 'active',
|
|
248
|
+
sourceScopeUri: group.sourceCanonical,
|
|
249
|
+
targetScopeUri: group.targetCanonical,
|
|
250
|
+
group: [{
|
|
251
|
+
source: group.sourceCanonical,
|
|
252
|
+
target: group.targetCanonical,
|
|
253
|
+
element: elements
|
|
254
|
+
}]
|
|
255
|
+
};
|
|
256
|
+
if (group.lastUpdated) {
|
|
257
|
+
json.meta = { lastUpdated: group.lastUpdated };
|
|
123
258
|
}
|
|
259
|
+
|
|
260
|
+
const cm = new ConceptMap(json, 'R5');
|
|
261
|
+
this.#indexConceptMap(cm);
|
|
262
|
+
results.push(cm);
|
|
124
263
|
}
|
|
125
264
|
return results;
|
|
126
265
|
}
|
|
127
266
|
|
|
267
|
+
#lastSegment(canonical) {
|
|
268
|
+
const raw = String(canonical || '').trim().replace(/\/+$/, '');
|
|
269
|
+
const slash = raw.lastIndexOf('/');
|
|
270
|
+
return slash >= 0 && slash < raw.length - 1 ? raw.substring(slash + 1) : raw;
|
|
271
|
+
}
|
|
272
|
+
|
|
128
273
|
async findConceptMapForTranslation(opContext, conceptMaps, sourceSystem, sourceScope, targetScope, targetSystem, sourceCode = null) {
|
|
274
|
+
try {
|
|
275
|
+
await this.#doFindConceptMapForTranslation(opContext, conceptMaps, sourceSystem, sourceScope, targetScope, targetSystem, sourceCode);
|
|
276
|
+
} catch (_err) {
|
|
277
|
+
// OCL API errors must not break $translate for other providers
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async #doFindConceptMapForTranslation(opContext, conceptMaps, sourceSystem, sourceScope, targetScope, targetSystem, sourceCode) {
|
|
129
282
|
const sourceCandidates = await this.#candidateSourceUrls(sourceSystem);
|
|
130
283
|
const targetCandidates = await this.#candidateSourceUrls(targetSystem);
|
|
131
284
|
|
|
@@ -135,8 +288,12 @@ class OCLConceptMapProvider extends AbstractConceptMapProvider {
|
|
|
135
288
|
if (sourceCode && sourcePaths.length > 0) {
|
|
136
289
|
for (const sourcePath of sourcePaths) {
|
|
137
290
|
const conceptPath = `${this.#normalizeSourcePath(sourcePath)}concepts/${encodeURIComponent(sourceCode)}/mappings/`;
|
|
138
|
-
|
|
139
|
-
|
|
291
|
+
try {
|
|
292
|
+
const found = await this.#fetchAllPages(conceptPath, { limit: PAGE_SIZE }, Math.min(2, this.maxSearchPages));
|
|
293
|
+
mappings.push(...found);
|
|
294
|
+
} catch (_err) {
|
|
295
|
+
// concept not found or mappings endpoint unavailable
|
|
296
|
+
}
|
|
140
297
|
}
|
|
141
298
|
}
|
|
142
299
|
|
|
@@ -311,43 +468,6 @@ class OCLConceptMapProvider extends AbstractConceptMapProvider {
|
|
|
311
468
|
}
|
|
312
469
|
}
|
|
313
470
|
|
|
314
|
-
#matches(json, params) {
|
|
315
|
-
for (const [name, value] of Object.entries(params)) {
|
|
316
|
-
if (!value) {
|
|
317
|
-
continue;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
if (name === 'url') {
|
|
321
|
-
if ((json.url || '').toLowerCase() !== value) {
|
|
322
|
-
return false;
|
|
323
|
-
}
|
|
324
|
-
continue;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
if (name === 'source') {
|
|
328
|
-
const src = json.group?.[0]?.source || '';
|
|
329
|
-
if (!src.toLowerCase().includes(value)) {
|
|
330
|
-
return false;
|
|
331
|
-
}
|
|
332
|
-
continue;
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
if (name === 'target') {
|
|
336
|
-
const tgt = json.group?.[0]?.target || '';
|
|
337
|
-
if (!tgt.toLowerCase().includes(value)) {
|
|
338
|
-
return false;
|
|
339
|
-
}
|
|
340
|
-
continue;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
const field = json[name];
|
|
344
|
-
if (field == null || !String(field).toLowerCase().includes(value)) {
|
|
345
|
-
return false;
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
return true;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
471
|
async #searchMappings(params = {}, maxPages = this.maxSearchPages) {
|
|
352
472
|
const endpoint = this.org ? `/orgs/${encodeURIComponent(this.org)}/mappings/` : '/mappings/';
|
|
353
473
|
return await this.#fetchAllPages(endpoint, params, maxPages);
|
package/tx/ocl/cs-ocl.cjs
CHANGED
|
@@ -48,6 +48,8 @@ class OCLCodeSystemProvider extends AbstractCodeSystemProvider {
|
|
|
48
48
|
this._pendingChanges = null;
|
|
49
49
|
this._initialized = false;
|
|
50
50
|
this._initializePromise = null;
|
|
51
|
+
this._organizationIdsCache = null;
|
|
52
|
+
this._organizationIdsFetchPromise = null;
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
async initialize() {
|
|
@@ -206,35 +208,54 @@ class OCLCodeSystemProvider extends AbstractCodeSystemProvider {
|
|
|
206
208
|
}
|
|
207
209
|
|
|
208
210
|
async #fetchOrganizationIds() {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
+
// Return cached result if available
|
|
212
|
+
if (this._organizationIdsCache) {
|
|
213
|
+
return this._organizationIdsCache;
|
|
214
|
+
}
|
|
211
215
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
continue;
|
|
217
|
-
}
|
|
216
|
+
// Deduplicate concurrent requests
|
|
217
|
+
if (this._organizationIdsFetchPromise) {
|
|
218
|
+
return this._organizationIdsFetchPromise;
|
|
219
|
+
}
|
|
218
220
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
221
|
+
this._organizationIdsFetchPromise = (async () => {
|
|
222
|
+
const endpoint = '/orgs/';
|
|
223
|
+
const orgs = await this.#fetchAllPages(endpoint);
|
|
224
|
+
|
|
225
|
+
const ids = [];
|
|
226
|
+
const seen = new Set();
|
|
227
|
+
for (const org of orgs || []) {
|
|
228
|
+
if (!org || typeof org !== 'object') {
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const id = org.id || org.mnemonic || org.short_code || org.shortCode || org.name || null;
|
|
233
|
+
if (!id) {
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const normalized = String(id).trim();
|
|
238
|
+
if (!normalized || seen.has(normalized)) {
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
seen.add(normalized);
|
|
243
|
+
ids.push(normalized);
|
|
222
244
|
}
|
|
223
245
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
continue;
|
|
246
|
+
if (ids.length === 0 && this.org) {
|
|
247
|
+
ids.push(this.org);
|
|
227
248
|
}
|
|
228
249
|
|
|
229
|
-
|
|
230
|
-
ids
|
|
231
|
-
}
|
|
250
|
+
this._organizationIdsCache = ids;
|
|
251
|
+
return ids;
|
|
252
|
+
})();
|
|
232
253
|
|
|
233
|
-
|
|
234
|
-
|
|
254
|
+
try {
|
|
255
|
+
return await this._organizationIdsFetchPromise;
|
|
256
|
+
} finally {
|
|
257
|
+
this._organizationIdsFetchPromise = null;
|
|
235
258
|
}
|
|
236
|
-
|
|
237
|
-
return ids;
|
|
238
259
|
}
|
|
239
260
|
|
|
240
261
|
#sourceIdentity(source) {
|
|
@@ -673,16 +694,33 @@ class OCLSourceCodeSystemProvider extends CodeSystemProvider {
|
|
|
673
694
|
async designations(code, displays) {
|
|
674
695
|
const ctxt = await this.#ensureContext(code);
|
|
675
696
|
if (ctxt && ctxt.display) {
|
|
676
|
-
const hasConceptDesignations = Array.isArray(ctxt.
|
|
697
|
+
const hasConceptDesignations = Array.isArray(ctxt.designation) && ctxt.designation.length > 0;
|
|
677
698
|
if (hasConceptDesignations) {
|
|
678
|
-
|
|
699
|
+
let hasNoLanguageEntry = false;
|
|
700
|
+
for (const d of ctxt.designation) {
|
|
679
701
|
if (!d || !d.value) {
|
|
680
702
|
continue;
|
|
681
703
|
}
|
|
682
704
|
displays.addDesignation(true, 'active', d.language || '', CodeSystem.makeUseForDisplay(), d.value);
|
|
705
|
+
if (!d.language) {
|
|
706
|
+
hasNoLanguageEntry = true;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
// Guarantee a language-neutral fallback so preferredDesignation() always returns
|
|
710
|
+
// a display value when the requested language has no matching designation.
|
|
711
|
+
// This implements the FHIR graceful-fallback rule for displayLanguage.
|
|
712
|
+
if (!hasNoLanguageEntry) {
|
|
713
|
+
displays.addDesignation(true, 'active', '', CodeSystem.makeUseForDisplay(), ctxt.display);
|
|
683
714
|
}
|
|
684
715
|
} else {
|
|
685
|
-
|
|
716
|
+
// No structured designations available. Use the source's configured language
|
|
717
|
+
// rather than hard-coding 'en' to avoid mislabeling non-English displays as English.
|
|
718
|
+
const defaultLang = this.meta?.codeSystem?.jsonObj?.language || '';
|
|
719
|
+
displays.addDesignation(true, 'active', defaultLang, CodeSystem.makeUseForDisplay(), ctxt.display);
|
|
720
|
+
// Also provide a no-language fallback for graceful language resolution.
|
|
721
|
+
if (defaultLang) {
|
|
722
|
+
displays.addDesignation(true, 'active', '', CodeSystem.makeUseForDisplay(), ctxt.display);
|
|
723
|
+
}
|
|
686
724
|
}
|
|
687
725
|
this._listSupplementDesignations(ctxt.code, displays);
|
|
688
726
|
}
|
|
@@ -914,7 +952,7 @@ class OCLSourceCodeSystemProvider extends CodeSystemProvider {
|
|
|
914
952
|
if (!this.meta.conceptsUrl) {
|
|
915
953
|
return [];
|
|
916
954
|
}
|
|
917
|
-
const cacheKey = `${this.meta.conceptsUrl}|p=${page}|l=${CONCEPT_PAGE_SIZE}`;
|
|
955
|
+
const cacheKey = `${this.meta.conceptsUrl}|p=${page}|l=${CONCEPT_PAGE_SIZE}|verbose=1`;
|
|
918
956
|
if (this.pageCache.has(cacheKey)) {
|
|
919
957
|
const cached = this.pageCache.get(cacheKey);
|
|
920
958
|
return Array.isArray(cached)
|
|
@@ -939,7 +977,7 @@ class OCLSourceCodeSystemProvider extends CodeSystemProvider {
|
|
|
939
977
|
const pending = (async () => {
|
|
940
978
|
let response;
|
|
941
979
|
try {
|
|
942
|
-
response = await this.httpClient.get(this.meta.conceptsUrl, { params: { page, limit: CONCEPT_PAGE_SIZE } });
|
|
980
|
+
response = await this.httpClient.get(this.meta.conceptsUrl, { params: { page, limit: CONCEPT_PAGE_SIZE, verbose: true } });
|
|
943
981
|
} catch (error) {
|
|
944
982
|
// Some OCL instances return 404 for sources without concept listing endpoints.
|
|
945
983
|
// Treat this as an empty page so terminology operations degrade gracefully.
|
|
@@ -998,7 +1036,7 @@ class OCLSourceCodeSystemProvider extends CodeSystemProvider {
|
|
|
998
1036
|
const pending = (async () => {
|
|
999
1037
|
let response;
|
|
1000
1038
|
try {
|
|
1001
|
-
response = await this.httpClient.get(url);
|
|
1039
|
+
response = await this.httpClient.get(url, { params: { verbose: true } });
|
|
1002
1040
|
} catch (error) {
|
|
1003
1041
|
// Missing concept should be treated as not-found, not as an internal server failure.
|
|
1004
1042
|
if (error && error.response && error.response.status === 404) {
|
|
@@ -1302,7 +1340,6 @@ class OCLSourceCodeSystemFactory extends CodeSystemFactoryProvider {
|
|
|
1302
1340
|
|
|
1303
1341
|
this.#applyConceptsToCodeSystemResource(this.meta?.codeSystem || null);
|
|
1304
1342
|
|
|
1305
|
-
console.log(`[OCL] Loaded CodeSystem from cold cache: ${canonicalUrl} (${cached.concepts.length} concepts, fingerprint=${this.customFingerprint?.substring(0, 8)})`);
|
|
1306
1343
|
} catch (error) {
|
|
1307
1344
|
if (error.code !== 'ENOENT') {
|
|
1308
1345
|
console.error(`[OCL] Failed to load cold cache for CodeSystem ${canonicalUrl}:`, error.message);
|
|
@@ -1329,7 +1366,6 @@ class OCLSourceCodeSystemFactory extends CodeSystemFactoryProvider {
|
|
|
1329
1366
|
};
|
|
1330
1367
|
|
|
1331
1368
|
await fs.writeFile(cacheFilePath, JSON.stringify(cacheData, null, 2), 'utf-8');
|
|
1332
|
-
console.log(`[OCL] Saved CodeSystem to cold cache: ${canonicalUrl} (${concepts.length} concepts, fingerprint=${fingerprint?.substring(0, 8)})`);
|
|
1333
1369
|
|
|
1334
1370
|
return fingerprint;
|
|
1335
1371
|
} catch (error) {
|
|
@@ -1343,7 +1379,6 @@ class OCLSourceCodeSystemFactory extends CodeSystemFactoryProvider {
|
|
|
1343
1379
|
const key = `${normalizedSystem}|${version || ''}`;
|
|
1344
1380
|
const factory = OCLSourceCodeSystemFactory.#findFactory(normalizedSystem, version);
|
|
1345
1381
|
if (!factory) {
|
|
1346
|
-
console.log(`[OCL] CodeSystem load not scheduled (factory unavailable): ${key}`);
|
|
1347
1382
|
return false;
|
|
1348
1383
|
}
|
|
1349
1384
|
factory.scheduleBackgroundLoad(reason);
|
|
@@ -1412,7 +1447,6 @@ class OCLSourceCodeSystemFactory extends CodeSystemFactoryProvider {
|
|
|
1412
1447
|
}
|
|
1413
1448
|
|
|
1414
1449
|
if (cacheAgeMs != null && cacheAgeMs < COLD_CACHE_FRESHNESS_MS) {
|
|
1415
|
-
console.log(`[OCL] Skipping warm-up for CodeSystem ${this.system()} (cold cache age: ${formatCacheAgeMinutes(cacheAgeMs)})`);
|
|
1416
1450
|
return;
|
|
1417
1451
|
}
|
|
1418
1452
|
|
|
@@ -1420,12 +1454,10 @@ class OCLSourceCodeSystemFactory extends CodeSystemFactoryProvider {
|
|
|
1420
1454
|
const jobKey = `cs:${key}`;
|
|
1421
1455
|
|
|
1422
1456
|
if (OCLBackgroundJobQueue.isQueuedOrRunning(jobKey)) {
|
|
1423
|
-
console.log(`[OCL] CodeSystem load already queued or running: ${key}`);
|
|
1424
1457
|
return;
|
|
1425
1458
|
}
|
|
1426
1459
|
|
|
1427
1460
|
let queuedJobSize = null;
|
|
1428
|
-
console.log(`[OCL] CodeSystem load enqueued: ${key} (${reason})`);
|
|
1429
1461
|
OCLBackgroundJobQueue.enqueue(
|
|
1430
1462
|
jobKey,
|
|
1431
1463
|
'CodeSystem load',
|
|
@@ -1444,7 +1476,6 @@ class OCLSourceCodeSystemFactory extends CodeSystemFactoryProvider {
|
|
|
1444
1476
|
}
|
|
1445
1477
|
|
|
1446
1478
|
async #runBackgroundLoad(key, knownConceptCount = null) {
|
|
1447
|
-
console.log(`[OCL] CodeSystem load started: ${key}`);
|
|
1448
1479
|
try {
|
|
1449
1480
|
this.backgroundLoadProgress = { processed: 0, total: null };
|
|
1450
1481
|
const resolvedTotal = Number.isFinite(knownConceptCount) && knownConceptCount >= 0
|
|
@@ -1470,16 +1501,7 @@ class OCLSourceCodeSystemFactory extends CodeSystemFactoryProvider {
|
|
|
1470
1501
|
const allConcepts = Array.from(this.sharedConceptCache.values());
|
|
1471
1502
|
const newFingerprint = computeCodeSystemFingerprint(allConcepts);
|
|
1472
1503
|
|
|
1473
|
-
if (this.customFingerprint
|
|
1474
|
-
console.log(`[OCL] CodeSystem fingerprint unchanged: ${key} (fingerprint=${newFingerprint?.substring(0, 8)})`);
|
|
1475
|
-
} else {
|
|
1476
|
-
if (this.customFingerprint) {
|
|
1477
|
-
console.log(`[OCL] CodeSystem fingerprint changed: ${key} (${this.customFingerprint?.substring(0, 8)} -> ${newFingerprint?.substring(0, 8)})`);
|
|
1478
|
-
console.log(`[OCL] Replacing cold cache with new hot cache: ${key}`);
|
|
1479
|
-
} else {
|
|
1480
|
-
console.log(`[OCL] Computed fingerprint for CodeSystem: ${key} (fingerprint=${newFingerprint?.substring(0, 8)})`);
|
|
1481
|
-
}
|
|
1482
|
-
|
|
1504
|
+
if (!this.customFingerprint || newFingerprint !== this.customFingerprint) {
|
|
1483
1505
|
// Save to cold cache
|
|
1484
1506
|
const savedFingerprint = await this.#saveColdCache(allConcepts);
|
|
1485
1507
|
if (savedFingerprint) {
|
|
@@ -1487,10 +1509,7 @@ class OCLSourceCodeSystemFactory extends CodeSystemFactoryProvider {
|
|
|
1487
1509
|
}
|
|
1488
1510
|
}
|
|
1489
1511
|
|
|
1490
|
-
console.log(`[OCL] CodeSystem
|
|
1491
|
-
const progress = OCLSourceCodeSystemFactory.loadProgress();
|
|
1492
|
-
console.log(`[OCL] CodeSystem load completed: ${this.system()}. Loaded ${progress.loaded}/${progress.total} CodeSystems (${progress.percentage.toFixed(2)}%)`);
|
|
1493
|
-
console.log(`[OCL] CodeSystem now available in cache: ${key} (${count} concepts)`);
|
|
1512
|
+
console.log(`[OCL] CodeSystem loaded: ${this.system()} (${count} concepts)`);
|
|
1494
1513
|
} catch (error) {
|
|
1495
1514
|
console.error(`[OCL] CodeSystem background load failed: ${key}: ${error.message}`);
|
|
1496
1515
|
}
|
|
@@ -1525,7 +1544,7 @@ class OCLSourceCodeSystemFactory extends CodeSystemFactoryProvider {
|
|
|
1525
1544
|
}
|
|
1526
1545
|
|
|
1527
1546
|
async #fetchAndCacheConceptPage(page) {
|
|
1528
|
-
const cacheKey = `${this.meta.conceptsUrl}|p=${page}|l=${CONCEPT_PAGE_SIZE}`;
|
|
1547
|
+
const cacheKey = `${this.meta.conceptsUrl}|p=${page}|l=${CONCEPT_PAGE_SIZE}|verbose=1`;
|
|
1529
1548
|
if (this.sharedPageCache.has(cacheKey)) {
|
|
1530
1549
|
const cached = this.sharedPageCache.get(cacheKey);
|
|
1531
1550
|
const concepts = Array.isArray(cached)
|
|
@@ -1544,7 +1563,7 @@ class OCLSourceCodeSystemFactory extends CodeSystemFactoryProvider {
|
|
|
1544
1563
|
const pending = (async () => {
|
|
1545
1564
|
let response;
|
|
1546
1565
|
try {
|
|
1547
|
-
response = await this.httpClient.get(this.meta.conceptsUrl, { params: { page, limit: CONCEPT_PAGE_SIZE } });
|
|
1566
|
+
response = await this.httpClient.get(this.meta.conceptsUrl, { params: { page, limit: CONCEPT_PAGE_SIZE, verbose: true } });
|
|
1548
1567
|
} catch (error) {
|
|
1549
1568
|
if (error && error.response && error.response.status === 404) {
|
|
1550
1569
|
this.sharedPageCache.set(cacheKey, []);
|
|
@@ -34,7 +34,6 @@ class OCLBackgroundJobQueue {
|
|
|
34
34
|
};
|
|
35
35
|
this.#insertPendingJobOrdered(job);
|
|
36
36
|
this.ensureHeartbeatRunning();
|
|
37
|
-
console.log(`[OCL] ${jobType || 'Background job'} enqueued: ${jobKey} (size=${normalizedSize}, queue=${this.pendingJobs.length}, active=${this.activeCount})`);
|
|
38
37
|
this.processNext();
|
|
39
38
|
};
|
|
40
39
|
|
|
@@ -77,7 +76,6 @@ class OCLBackgroundJobQueue {
|
|
|
77
76
|
// Prioridade máxima para userRequested
|
|
78
77
|
if (job.userRequested) {
|
|
79
78
|
this.pendingJobs.unshift(job);
|
|
80
|
-
console.log(`[OCL] User-requested job prioritized: ${job.jobKey}`);
|
|
81
79
|
return;
|
|
82
80
|
}
|
|
83
81
|
let index = this.pendingJobs.findIndex(existing => {
|
|
@@ -179,13 +177,8 @@ class OCLBackgroundJobQueue {
|
|
|
179
177
|
getProgress: job.getProgress || null,
|
|
180
178
|
startedAt: Date.now()
|
|
181
179
|
});
|
|
182
|
-
console.log(`[OCL] Background job started: ${job.jobType} ${job.jobKey} (size=${job.jobSize}, queue=${this.pendingJobs.length}, active=${this.activeCount})`);
|
|
183
|
-
|
|
184
180
|
Promise.resolve()
|
|
185
181
|
.then(() => job.runJob())
|
|
186
|
-
.then(() => {
|
|
187
|
-
console.log(`[OCL] Background job completed: ${job.jobType} ${job.jobKey}`);
|
|
188
|
-
})
|
|
189
182
|
.catch((error) => {
|
|
190
183
|
const message = error && error.message ? error.message : String(error);
|
|
191
184
|
console.error(`[OCL] Background job failed: ${job.jobType} ${job.jobKey}: ${message}`);
|
|
@@ -194,7 +187,6 @@ class OCLBackgroundJobQueue {
|
|
|
194
187
|
this.activeCount -= 1;
|
|
195
188
|
this.queuedOrRunningKeys.delete(job.jobKey);
|
|
196
189
|
this.activeJobs.delete(job.jobKey);
|
|
197
|
-
console.log(`[OCL] Background queue status: queue=${this.pendingJobs.length}, active=${this.activeCount}`);
|
|
198
190
|
this.processNext();
|
|
199
191
|
});
|
|
200
192
|
}
|
|
@@ -13,13 +13,14 @@ function toConceptContext(concept) {
|
|
|
13
13
|
display: concept.display_name || concept.display || concept.name || null,
|
|
14
14
|
definition: concept.description || concept.definition || null,
|
|
15
15
|
retired: concept.retired === true,
|
|
16
|
-
|
|
16
|
+
designation: extractDesignations(concept)
|
|
17
17
|
};
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
function extractDesignations(concept) {
|
|
21
21
|
const result = [];
|
|
22
22
|
const seen = new Set();
|
|
23
|
+
const seenValues = new Set();
|
|
23
24
|
|
|
24
25
|
const add = (language, value) => {
|
|
25
26
|
const text = typeof value === 'string' ? value.trim() : '';
|
|
@@ -28,12 +29,19 @@ function extractDesignations(concept) {
|
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
const lang = typeof language === 'string' ? language.trim() : '';
|
|
32
|
+
|
|
33
|
+
// Skip empty-language entries whose value already appears under any language
|
|
34
|
+
if (!lang && seenValues.has(text)) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
31
38
|
const key = `${lang}|${text}`;
|
|
32
39
|
if (seen.has(key)) {
|
|
33
40
|
return;
|
|
34
41
|
}
|
|
35
42
|
|
|
36
43
|
seen.add(key);
|
|
44
|
+
seenValues.add(text);
|
|
37
45
|
result.push({ language: lang, value: text });
|
|
38
46
|
};
|
|
39
47
|
|
|
@@ -42,8 +50,10 @@ function extractDesignations(concept) {
|
|
|
42
50
|
if (!entry || typeof entry !== 'object') {
|
|
43
51
|
continue;
|
|
44
52
|
}
|
|
45
|
-
|
|
46
|
-
|
|
53
|
+
// Prioriza locale como language
|
|
54
|
+
const lang = entry.locale;
|
|
55
|
+
const value = entry.name;
|
|
56
|
+
add(lang, value);
|
|
47
57
|
}
|
|
48
58
|
}
|
|
49
59
|
|