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.
Files changed (62) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +2 -0
  3. package/configurations/projector.json +21 -0
  4. package/configurations/readme.md +5 -0
  5. package/library/package-manager.js +0 -2
  6. package/library/version-utilities.js +85 -0
  7. package/package.json +1 -1
  8. package/packages/package-crawler.js +44 -9
  9. package/packages/packages.js +1 -0
  10. package/registry/crawler.js +35 -14
  11. package/registry/registry.js +3 -0
  12. package/server.js +4 -0
  13. package/tx/README.md +4 -4
  14. package/tx/cs/cs-loinc.js +5 -2
  15. package/tx/cs/cs-provider-api.js +25 -1
  16. package/tx/cs/cs-provider-list.js +2 -2
  17. package/tx/library/canonical-resource.js +6 -1
  18. package/tx/library.js +127 -10
  19. package/tx/ocl/README.md +236 -0
  20. package/tx/ocl/cache/cache-paths.cjs +32 -0
  21. package/tx/ocl/cache/cache-paths.js +2 -0
  22. package/tx/ocl/cache/cache-utils.cjs +43 -0
  23. package/tx/ocl/cache/cache-utils.js +2 -0
  24. package/tx/ocl/cm-ocl.cjs +531 -0
  25. package/tx/ocl/cm-ocl.js +1 -105
  26. package/tx/ocl/cs-ocl.cjs +1779 -0
  27. package/tx/ocl/cs-ocl.js +1 -38
  28. package/tx/ocl/fingerprint/fingerprint.cjs +67 -0
  29. package/tx/ocl/fingerprint/fingerprint.js +2 -0
  30. package/tx/ocl/http/client.cjs +31 -0
  31. package/tx/ocl/http/client.js +2 -0
  32. package/tx/ocl/http/pagination.cjs +98 -0
  33. package/tx/ocl/http/pagination.js +2 -0
  34. package/tx/ocl/jobs/background-queue.cjs +200 -0
  35. package/tx/ocl/jobs/background-queue.js +2 -0
  36. package/tx/ocl/mappers/concept-mapper.cjs +66 -0
  37. package/tx/ocl/mappers/concept-mapper.js +2 -0
  38. package/tx/ocl/model/concept-filter-context.cjs +51 -0
  39. package/tx/ocl/model/concept-filter-context.js +2 -0
  40. package/tx/ocl/shared/constants.cjs +15 -0
  41. package/tx/ocl/shared/constants.js +2 -0
  42. package/tx/ocl/shared/patches.cjs +224 -0
  43. package/tx/ocl/shared/patches.js +2 -0
  44. package/tx/ocl/vs-ocl.cjs +1848 -0
  45. package/tx/ocl/vs-ocl.js +1 -104
  46. package/tx/operation-context.js +8 -1
  47. package/tx/params.js +24 -3
  48. package/tx/provider.js +47 -0
  49. package/tx/tx-html.js +1 -1
  50. package/tx/tx.js +8 -0
  51. package/tx/vs/vs-vsac.js +4 -3
  52. package/tx/workers/batch-validate.js +3 -2
  53. package/tx/workers/batch.js +3 -2
  54. package/tx/workers/expand.js +64 -9
  55. package/tx/workers/lookup.js +5 -4
  56. package/tx/workers/read.js +2 -1
  57. package/tx/workers/related.js +3 -2
  58. package/tx/workers/search.js +4 -9
  59. package/tx/workers/subsumes.js +3 -2
  60. package/tx/workers/translate.js +4 -3
  61. package/tx/workers/validate.js +132 -40
  62. 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
- };