fhirsmith 0.6.0 → 0.7.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 +20 -0
- package/README.md +2 -0
- package/configurations/projector.json +21 -0
- package/configurations/readme.md +5 -0
- package/library/package-manager.js +0 -2
- package/library/version-utilities.js +85 -0
- package/package.json +1 -1
- package/packages/package-crawler.js +44 -9
- package/packages/packages.js +1 -0
- package/registry/crawler.js +35 -14
- package/registry/registry.js +3 -0
- package/server.js +4 -0
- package/tx/README.md +4 -4
- package/tx/cs/cs-loinc.js +5 -2
- package/tx/cs/cs-provider-api.js +25 -1
- package/tx/cs/cs-provider-list.js +2 -2
- package/tx/library/canonical-resource.js +6 -1
- package/tx/library.js +127 -10
- package/tx/ocl/README.md +236 -0
- package/tx/ocl/cache/cache-paths.cjs +32 -0
- package/tx/ocl/cache/cache-paths.js +2 -0
- package/tx/ocl/cache/cache-utils.cjs +43 -0
- package/tx/ocl/cache/cache-utils.js +2 -0
- package/tx/ocl/cm-ocl.cjs +531 -0
- package/tx/ocl/cm-ocl.js +1 -105
- package/tx/ocl/cs-ocl.cjs +1779 -0
- package/tx/ocl/cs-ocl.js +1 -38
- package/tx/ocl/fingerprint/fingerprint.cjs +67 -0
- package/tx/ocl/fingerprint/fingerprint.js +2 -0
- package/tx/ocl/http/client.cjs +31 -0
- package/tx/ocl/http/client.js +2 -0
- package/tx/ocl/http/pagination.cjs +98 -0
- package/tx/ocl/http/pagination.js +2 -0
- package/tx/ocl/jobs/background-queue.cjs +200 -0
- package/tx/ocl/jobs/background-queue.js +2 -0
- package/tx/ocl/mappers/concept-mapper.cjs +66 -0
- package/tx/ocl/mappers/concept-mapper.js +2 -0
- package/tx/ocl/model/concept-filter-context.cjs +51 -0
- package/tx/ocl/model/concept-filter-context.js +2 -0
- package/tx/ocl/shared/constants.cjs +15 -0
- package/tx/ocl/shared/constants.js +2 -0
- package/tx/ocl/shared/patches.cjs +224 -0
- package/tx/ocl/shared/patches.js +2 -0
- package/tx/ocl/vs-ocl.cjs +1848 -0
- package/tx/ocl/vs-ocl.js +1 -104
- package/tx/operation-context.js +8 -1
- package/tx/params.js +24 -3
- package/tx/provider.js +47 -0
- package/tx/tx-html.js +1 -1
- package/tx/tx.js +8 -0
- package/tx/vs/vs-vsac.js +4 -3
- package/tx/workers/batch-validate.js +3 -2
- package/tx/workers/batch.js +3 -2
- package/tx/workers/expand.js +64 -9
- package/tx/workers/lookup.js +5 -4
- package/tx/workers/read.js +2 -1
- package/tx/workers/related.js +3 -2
- package/tx/workers/search.js +4 -9
- package/tx/workers/subsumes.js +3 -2
- package/tx/workers/translate.js +4 -3
- package/tx/workers/validate.js +132 -40
- package/tx/workers/worker.js +1 -7
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
const { AbstractConceptMapProvider } = require('../cm/cm-api');
|
|
2
|
+
const { ConceptMap } = require('../library/conceptmap');
|
|
3
|
+
const { PAGE_SIZE } = require('./shared/constants');
|
|
4
|
+
const { createOclHttpClient } = require('./http/client');
|
|
5
|
+
const { fetchAllPages, extractItemsAndNext } = require('./http/pagination');
|
|
6
|
+
|
|
7
|
+
const DEFAULT_MAX_SEARCH_PAGES = 10;
|
|
8
|
+
|
|
9
|
+
class OCLConceptMapProvider extends AbstractConceptMapProvider {
|
|
10
|
+
constructor(config = {}) {
|
|
11
|
+
super();
|
|
12
|
+
const options = typeof config === 'string' ? { baseUrl: config } : (config || {});
|
|
13
|
+
|
|
14
|
+
this.org = options.org || null;
|
|
15
|
+
this.maxSearchPages = options.maxSearchPages || DEFAULT_MAX_SEARCH_PAGES;
|
|
16
|
+
const http = createOclHttpClient(options);
|
|
17
|
+
this.baseUrl = http.baseUrl;
|
|
18
|
+
this.httpClient = http.client;
|
|
19
|
+
|
|
20
|
+
this.conceptMapMap = new Map();
|
|
21
|
+
this._idMap = new Map();
|
|
22
|
+
this._sourceCandidatesCache = new Map();
|
|
23
|
+
this._sourceUrlsByCanonical = new Map();
|
|
24
|
+
this._canonicalBySourceUrl = new Map();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
assignIds(ids) {
|
|
28
|
+
if (!this.spaceId) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const unique = new Set(this.conceptMapMap.values());
|
|
33
|
+
this._idMap.clear();
|
|
34
|
+
|
|
35
|
+
for (const cm of unique) {
|
|
36
|
+
if (!cm.id.startsWith(`${this.spaceId}-`)) {
|
|
37
|
+
const nextId = `${this.spaceId}-${cm.id}`;
|
|
38
|
+
cm.id = nextId;
|
|
39
|
+
cm.jsonObj.id = nextId;
|
|
40
|
+
}
|
|
41
|
+
this._idMap.set(cm.id, cm);
|
|
42
|
+
ids.add(`ConceptMap/${cm.id}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async fetchConceptMap(url, version) {
|
|
47
|
+
this._validateFetchParams(url, version);
|
|
48
|
+
|
|
49
|
+
const direct = this.conceptMapMap.get(`${url}|${version}`) || this.conceptMapMap.get(url);
|
|
50
|
+
if (direct) {
|
|
51
|
+
return direct;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const mappingId = this.#extractMappingId(url);
|
|
55
|
+
if (mappingId) {
|
|
56
|
+
return await this.fetchConceptMapById(mappingId);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const mappings = await this.#searchMappings({ from_source_url: url }, this.maxSearchPages);
|
|
60
|
+
for (const mapping of mappings) {
|
|
61
|
+
const cm = this.#toConceptMap(mapping);
|
|
62
|
+
if (cm) {
|
|
63
|
+
this.#indexConceptMap(cm);
|
|
64
|
+
if (cm.url === url && (!version || cm.version === version)) {
|
|
65
|
+
return cm;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async fetchConceptMapById(id) {
|
|
74
|
+
if (this._idMap.has(id)) {
|
|
75
|
+
return this._idMap.get(id);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let rawId = id;
|
|
79
|
+
if (this.spaceId && id.startsWith(`${this.spaceId}-`)) {
|
|
80
|
+
rawId = id.substring(this.spaceId.length + 1);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (this._idMap.has(rawId)) {
|
|
84
|
+
return this._idMap.get(rawId);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const response = await this.httpClient.get(`/mappings/${encodeURIComponent(rawId)}/`);
|
|
88
|
+
const cm = this.#toConceptMap(response.data);
|
|
89
|
+
if (!cm) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
this.#indexConceptMap(cm);
|
|
93
|
+
return cm;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// eslint-disable-next-line no-unused-vars
|
|
97
|
+
async searchConceptMaps(searchParams, _elements) {
|
|
98
|
+
this._validateSearchParams(searchParams);
|
|
99
|
+
|
|
100
|
+
const params = Object.fromEntries(searchParams.map(({ name, value }) => [name, String(value).toLowerCase()]));
|
|
101
|
+
const oclParams = {};
|
|
102
|
+
|
|
103
|
+
if (params.source) {
|
|
104
|
+
oclParams.from_source_url = params.source;
|
|
105
|
+
}
|
|
106
|
+
if (params.target) {
|
|
107
|
+
oclParams.to_source_url = params.target;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const mappings = await this.#searchMappings(oclParams, this.maxSearchPages);
|
|
111
|
+
const results = [];
|
|
112
|
+
for (const mapping of mappings) {
|
|
113
|
+
const cm = this.#toConceptMap(mapping);
|
|
114
|
+
if (!cm) {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
this.#indexConceptMap(cm);
|
|
118
|
+
if (this.#matches(cm.jsonObj, params)) {
|
|
119
|
+
results.push(cm);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return results;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async findConceptMapForTranslation(opContext, conceptMaps, sourceSystem, sourceScope, targetScope, targetSystem, sourceCode = null) {
|
|
126
|
+
const sourceCandidates = await this.#candidateSourceUrls(sourceSystem);
|
|
127
|
+
const targetCandidates = await this.#candidateSourceUrls(targetSystem);
|
|
128
|
+
|
|
129
|
+
const mappings = [];
|
|
130
|
+
const sourcePaths = sourceCandidates.filter(s => String(s || '').startsWith('/orgs/'));
|
|
131
|
+
|
|
132
|
+
if (sourceCode && sourcePaths.length > 0) {
|
|
133
|
+
for (const sourcePath of sourcePaths) {
|
|
134
|
+
const conceptPath = `${this.#normalizeSourcePath(sourcePath)}concepts/${encodeURIComponent(sourceCode)}/mappings/`;
|
|
135
|
+
const found = await this.#fetchAllPages(conceptPath, { limit: PAGE_SIZE }, Math.min(2, this.maxSearchPages));
|
|
136
|
+
mappings.push(...found);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const searchKeys = new Set();
|
|
141
|
+
const searches = [];
|
|
142
|
+
|
|
143
|
+
if (sourceCandidates.length === 0 && targetCandidates.length === 0) {
|
|
144
|
+
searches.push({});
|
|
145
|
+
} else if (targetCandidates.length === 0) {
|
|
146
|
+
for (const src of sourceCandidates) {
|
|
147
|
+
const key = `from:${src}`;
|
|
148
|
+
if (!searchKeys.has(key)) {
|
|
149
|
+
searchKeys.add(key);
|
|
150
|
+
searches.push({ from_source_url: src });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
} else if (sourceCandidates.length === 0) {
|
|
154
|
+
for (const tgt of targetCandidates) {
|
|
155
|
+
const key = `to:${tgt}`;
|
|
156
|
+
if (!searchKeys.has(key)) {
|
|
157
|
+
searchKeys.add(key);
|
|
158
|
+
searches.push({ to_source_url: tgt });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
} else {
|
|
162
|
+
for (const src of sourceCandidates) {
|
|
163
|
+
for (const tgt of targetCandidates) {
|
|
164
|
+
const key = `from:${src}|to:${tgt}`;
|
|
165
|
+
if (!searchKeys.has(key)) {
|
|
166
|
+
searchKeys.add(key);
|
|
167
|
+
searches.push({ from_source_url: src, to_source_url: tgt });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (mappings.length === 0) {
|
|
174
|
+
for (const search of searches) {
|
|
175
|
+
const found = await this.#searchMappings(search, Math.min(2, this.maxSearchPages));
|
|
176
|
+
mappings.push(...found);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const sourceUrlsToResolve = new Set();
|
|
181
|
+
for (const mapping of mappings) {
|
|
182
|
+
const fromSource = mapping?.from_source_url || mapping?.fromSourceUrl;
|
|
183
|
+
const toSource = mapping?.to_source_url || mapping?.toSourceUrl;
|
|
184
|
+
if (fromSource) {
|
|
185
|
+
sourceUrlsToResolve.add(fromSource);
|
|
186
|
+
}
|
|
187
|
+
if (toSource) {
|
|
188
|
+
sourceUrlsToResolve.add(toSource);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
await this.#ensureCanonicalForSourceUrls(sourceUrlsToResolve);
|
|
192
|
+
|
|
193
|
+
const seen = new Set(conceptMaps.map(cm => cm.id || cm.url));
|
|
194
|
+
for (const mapping of mappings) {
|
|
195
|
+
const cm = this.#toConceptMap(mapping);
|
|
196
|
+
if (!cm) {
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
this.#indexConceptMap(cm);
|
|
200
|
+
|
|
201
|
+
const key = cm.id || cm.url;
|
|
202
|
+
if (seen.has(key)) {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (this.#matchesTranslationRequest(cm, sourceSystem, sourceScope, targetScope, targetSystem, sourceCandidates, targetCandidates)) {
|
|
207
|
+
conceptMaps.push(cm);
|
|
208
|
+
seen.add(key);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
cmCount() {
|
|
214
|
+
return new Set(this.conceptMapMap.values()).size;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async close() {
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
#indexConceptMap(cm) {
|
|
221
|
+
this.conceptMapMap.set(cm.url, cm);
|
|
222
|
+
if (cm.version) {
|
|
223
|
+
this.conceptMapMap.set(`${cm.url}|${cm.version}`, cm);
|
|
224
|
+
}
|
|
225
|
+
this.conceptMapMap.set(cm.id, cm);
|
|
226
|
+
this._idMap.set(cm.id, cm);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
#toConceptMap(mapping) {
|
|
230
|
+
if (!mapping || typeof mapping !== 'object') {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const id = mapping.id;
|
|
235
|
+
if (!id) {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const url = mapping.url || `${this.baseUrl}/mappings/${id}`;
|
|
240
|
+
const source = mapping.from_source_url || mapping.fromSourceUrl || mapping.from_concept_url || mapping.fromConceptUrl || null;
|
|
241
|
+
const target = mapping.to_source_url || mapping.toSourceUrl || mapping.to_concept_url || mapping.toConceptUrl || null;
|
|
242
|
+
const sourceCode = mapping.from_concept_code || mapping.fromConceptCode;
|
|
243
|
+
const targetCode = mapping.to_concept_code || mapping.toConceptCode;
|
|
244
|
+
|
|
245
|
+
if (!source || !target || !sourceCode || !targetCode) {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const sourceDisplay = mapping.from_concept_name_resolved || mapping.fromConceptNameResolved || mapping.from_concept_name || mapping.fromConceptName || null;
|
|
250
|
+
const targetDisplay = mapping.to_concept_name_resolved || mapping.toConceptNameResolved || mapping.to_concept_name || mapping.toConceptName || null;
|
|
251
|
+
const sourceCanonical = this.#canonicalForSourceUrl(source) || source;
|
|
252
|
+
const targetCanonical = this.#canonicalForSourceUrl(target) || target;
|
|
253
|
+
|
|
254
|
+
const relationship = this.#toRelationship(mapping.map_type || mapping.mapType);
|
|
255
|
+
const lastUpdated = this.#toIsoDate(mapping.updated_on || mapping.updatedOn || mapping.updated_at || mapping.updatedAt);
|
|
256
|
+
|
|
257
|
+
const json = {
|
|
258
|
+
resourceType: 'ConceptMap',
|
|
259
|
+
id,
|
|
260
|
+
url,
|
|
261
|
+
version: mapping.version || null,
|
|
262
|
+
name: `mapping-${id}`,
|
|
263
|
+
title: mapping.name || `Mapping ${id}`,
|
|
264
|
+
status: 'active',
|
|
265
|
+
sourceScopeUri: mapping.from_collection_url || mapping.fromCollectionUrl || source,
|
|
266
|
+
targetScopeUri: mapping.to_collection_url || mapping.toCollectionUrl || target,
|
|
267
|
+
group: [
|
|
268
|
+
{
|
|
269
|
+
source: sourceCanonical,
|
|
270
|
+
target: targetCanonical,
|
|
271
|
+
element: [
|
|
272
|
+
{
|
|
273
|
+
code: sourceCode,
|
|
274
|
+
display: sourceDisplay,
|
|
275
|
+
target: [
|
|
276
|
+
{
|
|
277
|
+
code: targetCode,
|
|
278
|
+
display: targetDisplay,
|
|
279
|
+
relationship,
|
|
280
|
+
comment: mapping.comment || null
|
|
281
|
+
}
|
|
282
|
+
]
|
|
283
|
+
}
|
|
284
|
+
]
|
|
285
|
+
}
|
|
286
|
+
]
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
if (lastUpdated) {
|
|
290
|
+
json.meta = { lastUpdated };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return new ConceptMap(json, 'R5');
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
#toRelationship(mapType) {
|
|
297
|
+
switch ((mapType || '').toUpperCase()) {
|
|
298
|
+
case 'SAME-AS':
|
|
299
|
+
return 'equivalent';
|
|
300
|
+
case 'NARROWER-THAN':
|
|
301
|
+
return 'narrower-than';
|
|
302
|
+
case 'BROADER-THAN':
|
|
303
|
+
return 'broader-than';
|
|
304
|
+
case 'NOT-EQUIVALENT':
|
|
305
|
+
return 'not-related-to';
|
|
306
|
+
default:
|
|
307
|
+
return 'related-to';
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
#matches(json, params) {
|
|
312
|
+
for (const [name, value] of Object.entries(params)) {
|
|
313
|
+
if (!value) {
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (name === 'url') {
|
|
318
|
+
if ((json.url || '').toLowerCase() !== value) {
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (name === 'source') {
|
|
325
|
+
const src = json.group?.[0]?.source || '';
|
|
326
|
+
if (!src.toLowerCase().includes(value)) {
|
|
327
|
+
return false;
|
|
328
|
+
}
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (name === 'target') {
|
|
333
|
+
const tgt = json.group?.[0]?.target || '';
|
|
334
|
+
if (!tgt.toLowerCase().includes(value)) {
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const field = json[name];
|
|
341
|
+
if (field == null || !String(field).toLowerCase().includes(value)) {
|
|
342
|
+
return false;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return true;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async #searchMappings(params = {}, maxPages = this.maxSearchPages) {
|
|
349
|
+
const endpoint = this.org ? `/orgs/${encodeURIComponent(this.org)}/mappings/` : '/mappings/';
|
|
350
|
+
return await this.#fetchAllPages(endpoint, params, maxPages);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async #fetchAllPages(path, params = {}, maxPages = this.maxSearchPages) {
|
|
354
|
+
return await fetchAllPages(this.httpClient, path, {
|
|
355
|
+
params,
|
|
356
|
+
pageSize: PAGE_SIZE,
|
|
357
|
+
maxPages,
|
|
358
|
+
baseUrl: this.baseUrl
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
#extractItemsAndNext(payload) {
|
|
363
|
+
return extractItemsAndNext(payload, this.baseUrl);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
#extractMappingId(url) {
|
|
367
|
+
if (!url) {
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
const match = url.match(/\/mappings\/([^/]+)\/?$/i);
|
|
371
|
+
return match ? match[1] : null;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async #candidateSourceUrls(systemUrl) {
|
|
375
|
+
if (!systemUrl) {
|
|
376
|
+
return [];
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const cacheKey = this.#norm(systemUrl);
|
|
380
|
+
if (this._sourceCandidatesCache.has(cacheKey)) {
|
|
381
|
+
return this._sourceCandidatesCache.get(cacheKey);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const result = new Set();
|
|
385
|
+
result.add(systemUrl);
|
|
386
|
+
|
|
387
|
+
const canonicalKey = cacheKey;
|
|
388
|
+
const byCanonical = this._sourceUrlsByCanonical.get(canonicalKey);
|
|
389
|
+
if (byCanonical) {
|
|
390
|
+
for (const item of byCanonical) {
|
|
391
|
+
result.add(item);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const discovered = await this.#resolveSourceCandidatesFromOcl(systemUrl);
|
|
396
|
+
for (const item of discovered) {
|
|
397
|
+
result.add(item);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const out = Array.from(result);
|
|
401
|
+
this._sourceCandidatesCache.set(cacheKey, out);
|
|
402
|
+
return out;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
async #resolveSourceCandidatesFromOcl(systemUrl) {
|
|
406
|
+
const endpoint = this.org ? `/orgs/${encodeURIComponent(this.org)}/sources/` : '/sources/';
|
|
407
|
+
const query = this.#queryTokenFromSystem(systemUrl);
|
|
408
|
+
if (!query) {
|
|
409
|
+
return [];
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const sources = await this.#fetchAllPages(endpoint, { q: query, limit: PAGE_SIZE }, 2);
|
|
413
|
+
const targetNorm = this.#norm(systemUrl);
|
|
414
|
+
const candidates = new Set();
|
|
415
|
+
|
|
416
|
+
for (const source of sources) {
|
|
417
|
+
const canonical = source?.canonical_url || source?.canonicalUrl || null;
|
|
418
|
+
const sourceUrl = source?.url || source?.uri || null;
|
|
419
|
+
if (!sourceUrl) {
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (canonical) {
|
|
424
|
+
const canonicalKey = this.#norm(canonical);
|
|
425
|
+
if (!this._sourceUrlsByCanonical.has(canonicalKey)) {
|
|
426
|
+
this._sourceUrlsByCanonical.set(canonicalKey, new Set());
|
|
427
|
+
}
|
|
428
|
+
this._sourceUrlsByCanonical.get(canonicalKey).add(sourceUrl);
|
|
429
|
+
this._canonicalBySourceUrl.set(this.#norm(sourceUrl), canonical);
|
|
430
|
+
|
|
431
|
+
if (canonicalKey === targetNorm) {
|
|
432
|
+
candidates.add(sourceUrl);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
if (this.#norm(sourceUrl) === targetNorm) {
|
|
437
|
+
candidates.add(sourceUrl);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return Array.from(candidates);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
async #ensureCanonicalForSourceUrls(sourceUrls) {
|
|
445
|
+
for (const sourceUrl of sourceUrls || []) {
|
|
446
|
+
const sourceKey = this.#norm(sourceUrl);
|
|
447
|
+
if (!sourceKey || this._canonicalBySourceUrl.has(sourceKey)) {
|
|
448
|
+
continue;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const sourcePath = String(sourceUrl || '').trim();
|
|
452
|
+
if (!sourcePath.startsWith('/orgs/')) {
|
|
453
|
+
continue;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
try {
|
|
457
|
+
const response = await this.httpClient.get(sourcePath);
|
|
458
|
+
const source = response.data || {};
|
|
459
|
+
const canonical = source.canonical_url || source.canonicalUrl;
|
|
460
|
+
const resolvedSourceUrl = source.url || source.uri || sourcePath;
|
|
461
|
+
if (!canonical) {
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const canonicalKey = this.#norm(canonical);
|
|
466
|
+
if (!this._sourceUrlsByCanonical.has(canonicalKey)) {
|
|
467
|
+
this._sourceUrlsByCanonical.set(canonicalKey, new Set());
|
|
468
|
+
}
|
|
469
|
+
this._sourceUrlsByCanonical.get(canonicalKey).add(resolvedSourceUrl);
|
|
470
|
+
this._canonicalBySourceUrl.set(this.#norm(resolvedSourceUrl), canonical);
|
|
471
|
+
} catch (e) {
|
|
472
|
+
// Ignore source lookup failures and continue resolving remaining sources.
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
#queryTokenFromSystem(systemUrl) {
|
|
479
|
+
const raw = String(systemUrl || '').trim().replace(/\/+$/, '');
|
|
480
|
+
if (!raw) {
|
|
481
|
+
return null;
|
|
482
|
+
}
|
|
483
|
+
const slash = raw.lastIndexOf('/');
|
|
484
|
+
if (slash >= 0 && slash < raw.length - 1) {
|
|
485
|
+
return raw.substring(slash + 1);
|
|
486
|
+
}
|
|
487
|
+
return raw;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
#normalizeSourcePath(sourcePath) {
|
|
491
|
+
const path = String(sourcePath || '').trim();
|
|
492
|
+
return path.endsWith('/') ? path : `${path}/`;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
#canonicalForSourceUrl(sourceUrl) {
|
|
496
|
+
return this._canonicalBySourceUrl.get(this.#norm(sourceUrl)) || null;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
#matchesTranslationRequest(cm, sourceSystem, sourceScope, targetScope, targetSystem, sourceCandidates, targetCandidates) {
|
|
500
|
+
if (cm.providesTranslation(sourceSystem, sourceScope, targetScope, targetSystem)) {
|
|
501
|
+
return true;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const group = cm.jsonObj?.group?.[0] || {};
|
|
505
|
+
const groupSource = this.#norm(group.source);
|
|
506
|
+
const groupTarget = this.#norm(group.target);
|
|
507
|
+
|
|
508
|
+
const sourceOk = !sourceSystem || sourceCandidates.some(s => this.#norm(s) === groupSource);
|
|
509
|
+
const targetOk = !targetSystem || targetCandidates.some(s => this.#norm(s) === groupTarget);
|
|
510
|
+
return sourceOk && targetOk;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
#norm(url) {
|
|
514
|
+
return String(url || '').trim().replace(/\/+$/, '').toLowerCase();
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
#toIsoDate(value) {
|
|
518
|
+
if (!value) {
|
|
519
|
+
return null;
|
|
520
|
+
}
|
|
521
|
+
const date = new Date(value);
|
|
522
|
+
if (Number.isNaN(date.getTime())) {
|
|
523
|
+
return null;
|
|
524
|
+
}
|
|
525
|
+
return date.toISOString();
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
module.exports = {
|
|
530
|
+
OCLConceptMapProvider
|
|
531
|
+
};
|
package/tx/ocl/cm-ocl.js
CHANGED
|
@@ -1,106 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
* Abstract base class for Concept Map providers
|
|
3
|
-
* Defines the interface that all Concept Map providers must implement
|
|
4
|
-
*/
|
|
5
|
-
// eslint-disable-next-line no-unused-vars
|
|
6
|
-
class OCLConceptMapProvider {
|
|
7
|
-
/**
|
|
8
|
-
* {int} Unique number assigned to this provider
|
|
9
|
-
*/
|
|
10
|
-
spaceId;
|
|
1
|
+
module.exports = require('./cm-ocl.cjs');
|
|
11
2
|
|
|
12
|
-
/**
|
|
13
|
-
* ensure that the ids on the Concept Maps are unique, if they are
|
|
14
|
-
* in the global namespace
|
|
15
|
-
*
|
|
16
|
-
* @param {Set<String>} ids
|
|
17
|
-
*/
|
|
18
|
-
// eslint-disable-next-line no-unused-vars
|
|
19
|
-
assignIds(ids) {
|
|
20
|
-
throw new Error('assignIds must be implemented by AbstractConceptMapProvider subclass');
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Fetches a specific Concept Map by URL and version
|
|
25
|
-
* @param {string} url - The URL/identifier of the Concept Map
|
|
26
|
-
* @param {string} version - The version of the Concept Map
|
|
27
|
-
* @returns {Promise<ConceptMap>} The requested Concept Map
|
|
28
|
-
* @throws {Error} Must be implemented by subclasses
|
|
29
|
-
*/
|
|
30
|
-
// eslint-disable-next-line no-unused-vars
|
|
31
|
-
async fetchConceptMap(url, version) {
|
|
32
|
-
throw new Error('fetchConceptMap must be implemented by subclass');
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Fetches a specific Concept Map by id. ConceptMap providers must enforce that Concept Map ids are unique
|
|
37
|
-
* either globally (as enforced by assignIds) or in their space
|
|
38
|
-
*
|
|
39
|
-
* @param {string} id - The id of the Concept Map
|
|
40
|
-
* @returns {Promise<ConceptMap>} The requested Concept Map
|
|
41
|
-
* @throws {Error} Must be implemented by subclasses
|
|
42
|
-
*/
|
|
43
|
-
// eslint-disable-next-line no-unused-vars
|
|
44
|
-
async fetchConceptMapById(id) {
|
|
45
|
-
throw new Error('fetchConceptMapById must be implemented by subclass');
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Searches for Concept Maps based on provided criteria
|
|
50
|
-
* @param {Array<{name: string, value: string}>} searchParams - List of name/value pairs for search criteria
|
|
51
|
-
* @returns {Promise<Array<ConceptMap>>} List of matching Concept Maps
|
|
52
|
-
* @throws {Error} Must be implemented by subclasses
|
|
53
|
-
*/
|
|
54
|
-
// eslint-disable-next-line no-unused-vars
|
|
55
|
-
async searchConceptMaps(searchParams, elements = null) {
|
|
56
|
-
throw new Error('searchConceptMaps must be implemented by subclass');
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Validates search parameters
|
|
61
|
-
* @param {Array<{name: string, value: string}>} searchParams - Search parameters to validate
|
|
62
|
-
* @protected
|
|
63
|
-
*/
|
|
64
|
-
_validateSearchParams(searchParams) {
|
|
65
|
-
if (!Array.isArray(searchParams)) {
|
|
66
|
-
throw new Error('Search parameters must be an array');
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
for (const param of searchParams) {
|
|
70
|
-
if (!param || typeof param !== 'object') {
|
|
71
|
-
throw new Error('Each search parameter must be an object');
|
|
72
|
-
}
|
|
73
|
-
if (typeof param.name !== 'string' || typeof param.value !== 'string') {
|
|
74
|
-
throw new Error('Search parameter must have string name and value properties');
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Validates URL and version parameters
|
|
81
|
-
* @param {string} url - URL to validate
|
|
82
|
-
* @param {string} version - Version to validate
|
|
83
|
-
* @protected
|
|
84
|
-
*/
|
|
85
|
-
_validateFetchParams(url, version) {
|
|
86
|
-
if (typeof url !== 'string' || !url.trim()) {
|
|
87
|
-
throw new Error('URL must be a non-empty string');
|
|
88
|
-
}
|
|
89
|
-
if (version != null && typeof version !== 'string') {
|
|
90
|
-
throw new Error('Version must be a string');
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// eslint-disable-next-line no-unused-vars
|
|
95
|
-
async findConceptMapForTranslation(opContext, conceptMaps, sourceSystem, sourceScope, targetScope, targetSystem) {
|
|
96
|
-
// nothing
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
cmCount() {
|
|
100
|
-
return 0;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
module.exports = {
|
|
105
|
-
AbstractConceptMapProvider
|
|
106
|
-
};
|