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
package/tx/cs/cs-provider-api.js
CHANGED
|
@@ -20,7 +20,13 @@ class AbstractCodeSystemProvider {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
|
-
|
|
23
|
+
* Returns the list of CodeSystems this provider provides. This is called once at start up.
|
|
24
|
+
* The code systems should be fully loaded; lazy loading code systems is not considered good
|
|
25
|
+
* for engineering.
|
|
26
|
+
*
|
|
27
|
+
*
|
|
28
|
+
* Note that unlike value sets, which are accessed from the provider on the fly, code systems
|
|
29
|
+
* are all preloaded into the kernel (e.g. provider) at start up
|
|
24
30
|
*
|
|
25
31
|
* @param {string} fhirVersion - The FHIRVersion in scope - if relevant (there's always a stated version, though R5 is always used)
|
|
26
32
|
* @param {string} context - The client's stated context - if provided.
|
|
@@ -32,6 +38,24 @@ class AbstractCodeSystemProvider {
|
|
|
32
38
|
throw new Error('listCodeSystems must be implemented by AbstractCodeSystemProvider subclass');
|
|
33
39
|
}
|
|
34
40
|
|
|
41
|
+
/**
|
|
42
|
+
* This is called once a minute to update the code system list that the provider maintains.
|
|
43
|
+
*
|
|
44
|
+
* return an object that has three Map<String, CodeSystem>: {added, changed, deleted}
|
|
45
|
+
*
|
|
46
|
+
* these use the same key as the
|
|
47
|
+
*
|
|
48
|
+
* code systems are identified by url and version
|
|
49
|
+
*
|
|
50
|
+
* @param fhirVersion
|
|
51
|
+
* @param context
|
|
52
|
+
* @returns {Promise<null>}
|
|
53
|
+
*/
|
|
54
|
+
// eslint-disable-next-line no-unused-vars
|
|
55
|
+
async getCodeSystemChanges(fhirVersion, context){
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
35
59
|
async close() {
|
|
36
60
|
|
|
37
61
|
}
|
|
@@ -7,7 +7,7 @@ class ListCodeSystemProvider extends AbstractCodeSystemProvider {
|
|
|
7
7
|
/**
|
|
8
8
|
* {Map<String, CodeSystem>} A list of code system factories that contains all the preloaded native code systems
|
|
9
9
|
*/
|
|
10
|
-
codeSystems =
|
|
10
|
+
codeSystems = [];
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* ensure that the ids on the code systems are unique, if they are
|
|
@@ -17,7 +17,7 @@ class ListCodeSystemProvider extends AbstractCodeSystemProvider {
|
|
|
17
17
|
*/
|
|
18
18
|
// eslint-disable-next-line no-unused-vars
|
|
19
19
|
assignIds(ids) {
|
|
20
|
-
for (const cs of this.codeSystems
|
|
20
|
+
for (const cs of this.codeSystems) {
|
|
21
21
|
if (!cs.id || ids.has("CodeSystem/"+cs.id)) {
|
|
22
22
|
cs.id = ""+ids.size;
|
|
23
23
|
}
|
|
@@ -111,7 +111,12 @@ class CanonicalResource {
|
|
|
111
111
|
const fmt = this.versionAlgorithm() || other.versionAlgorithm() || this.guessVersionAlgorithmFromVersion(this.version);
|
|
112
112
|
switch (fmt) {
|
|
113
113
|
case 'semver':
|
|
114
|
-
|
|
114
|
+
try {
|
|
115
|
+
return VersionUtilities.isThisOrLater(other.version, this.version, VersionPrecision.PATCH);
|
|
116
|
+
} catch (error) {
|
|
117
|
+
// other is not semver. Not much we can do
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
115
120
|
case 'date':
|
|
116
121
|
return this.dateIsMoreRecent(this.version, other.version);
|
|
117
122
|
case 'integer':
|
package/tx/library.js
CHANGED
|
@@ -31,6 +31,9 @@ const { Provider } = require("./provider");
|
|
|
31
31
|
const {I18nSupport} = require("../library/i18nsupport");
|
|
32
32
|
const folders = require('../library/folder-setup');
|
|
33
33
|
const {VSACValueSetProvider} = require("./vs/vs-vsac");
|
|
34
|
+
const { OCLCodeSystemProvider, OCLSourceCodeSystemFactory } = require('./ocl/cs-ocl');
|
|
35
|
+
const { OCLValueSetProvider } = require('./ocl/vs-ocl');
|
|
36
|
+
const { OCLConceptMapProvider } = require('./ocl/cm-ocl');
|
|
34
37
|
|
|
35
38
|
/**
|
|
36
39
|
* This class holds all the loaded content ready for processing
|
|
@@ -95,6 +98,8 @@ class Library {
|
|
|
95
98
|
this.codeSystemProviders = [];
|
|
96
99
|
this.valueSetProviders = [];
|
|
97
100
|
this.conceptMapProviders = [];
|
|
101
|
+
this.oclProviderSets = new Map();
|
|
102
|
+
this.oclConfig = {};
|
|
98
103
|
|
|
99
104
|
// Create package manager for FHIR packages
|
|
100
105
|
const packageServers = ['https://packages2.fhir.org/packages'];
|
|
@@ -154,6 +159,7 @@ class Library {
|
|
|
154
159
|
const yamlContent = await fs.readFile(yamlPath, 'utf8');
|
|
155
160
|
const config = yaml.parse(yamlContent);
|
|
156
161
|
this.baseUrl = config.base.url;
|
|
162
|
+
this.oclConfig = config.ocl && typeof config.ocl === 'object' ? config.ocl : {};
|
|
157
163
|
|
|
158
164
|
this.log.info('Fetching Data from '+this.baseUrl);
|
|
159
165
|
|
|
@@ -277,12 +283,127 @@ class Library {
|
|
|
277
283
|
case 'url/cs':
|
|
278
284
|
await this.loadUrl(packageManager, details, isDefault, mode, true);
|
|
279
285
|
break;
|
|
286
|
+
|
|
287
|
+
case 'ocl':
|
|
288
|
+
await this.loadOcl(details, isDefault, mode);
|
|
289
|
+
break;
|
|
280
290
|
|
|
281
291
|
default:
|
|
282
292
|
throw new Error(`Unknown source type: ${type}`);
|
|
283
293
|
}
|
|
284
294
|
}
|
|
285
295
|
|
|
296
|
+
parseOclConfig(details) {
|
|
297
|
+
const text = String(details || '').trim();
|
|
298
|
+
if (!text) {
|
|
299
|
+
throw new Error('OCL source requires details, e.g. ocl:https://ocl.example.org');
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const parts = text.split('|').map(p => p.trim()).filter(Boolean);
|
|
303
|
+
const baseUrl = this.resolveOclConfigValue(parts[0]);
|
|
304
|
+
if (!baseUrl) {
|
|
305
|
+
throw new Error('OCL source requires a base URL');
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const config = { baseUrl };
|
|
309
|
+
for (let i = 1; i < parts.length; i++) {
|
|
310
|
+
const part = parts[i];
|
|
311
|
+
const eq = part.indexOf('=');
|
|
312
|
+
if (eq === -1) {
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
const key = part.substring(0, eq).trim().toLowerCase();
|
|
316
|
+
const value = this.resolveOclConfigValue(part.substring(eq + 1).trim());
|
|
317
|
+
if (!value) {
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
if (key === 'org') {
|
|
321
|
+
config.org = value;
|
|
322
|
+
} else if (key === 'token') {
|
|
323
|
+
config.token = value;
|
|
324
|
+
} else if (key === 'timeout') {
|
|
325
|
+
const timeout = Number(value);
|
|
326
|
+
if (Number.isFinite(timeout) && timeout > 0) {
|
|
327
|
+
config.timeout = timeout;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return config;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
resolveOclConfigValue(value) {
|
|
336
|
+
const text = String(value || '').trim();
|
|
337
|
+
if (!text) {
|
|
338
|
+
return text;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (this.oclConfig && Object.hasOwn(this.oclConfig, text)) {
|
|
342
|
+
const resolved = this.oclConfig[text];
|
|
343
|
+
if (typeof resolved === 'string') {
|
|
344
|
+
return resolved.trim();
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return text;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async loadOcl(details, isDefault, mode) {
|
|
352
|
+
const config = this.parseOclConfig(details);
|
|
353
|
+
const cacheKey = `${config.baseUrl}|${config.org || ''}`;
|
|
354
|
+
|
|
355
|
+
let providerSet = this.oclProviderSets.get(cacheKey);
|
|
356
|
+
if (!providerSet) {
|
|
357
|
+
const codeSystemProvider = new OCLCodeSystemProvider(config);
|
|
358
|
+
const valueSetProvider = new OCLValueSetProvider(config);
|
|
359
|
+
const conceptMapProvider = new OCLConceptMapProvider(config);
|
|
360
|
+
providerSet = {
|
|
361
|
+
config,
|
|
362
|
+
codeSystemProvider,
|
|
363
|
+
valueSetProvider,
|
|
364
|
+
conceptMapProvider,
|
|
365
|
+
csRegistered: false,
|
|
366
|
+
factoriesRegistered: false,
|
|
367
|
+
vsRegistered: false,
|
|
368
|
+
cmRegistered: false
|
|
369
|
+
};
|
|
370
|
+
this.oclProviderSets.set(cacheKey, providerSet);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (mode === 'fetch') {
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (mode === 'cs') {
|
|
378
|
+
if (!providerSet.csRegistered) {
|
|
379
|
+
this.codeSystemProviders.push(providerSet.codeSystemProvider);
|
|
380
|
+
providerSet.csRegistered = true;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (!providerSet.factoriesRegistered) {
|
|
384
|
+
await providerSet.codeSystemProvider.listCodeSystems('5.0', null);
|
|
385
|
+
const metas = providerSet.codeSystemProvider.getSourceMetas();
|
|
386
|
+
for (const meta of metas) {
|
|
387
|
+
const factory = new OCLSourceCodeSystemFactory(this.i18n, providerSet.codeSystemProvider.httpClient, meta);
|
|
388
|
+
this.registerProvider(`ocl:${config.baseUrl}`, factory, isDefault);
|
|
389
|
+
}
|
|
390
|
+
providerSet.factoriesRegistered = true;
|
|
391
|
+
}
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (mode === 'npm') {
|
|
396
|
+
if (!providerSet.vsRegistered) {
|
|
397
|
+
this.valueSetProviders.push(providerSet.valueSetProvider);
|
|
398
|
+
providerSet.vsRegistered = true;
|
|
399
|
+
}
|
|
400
|
+
if (!providerSet.cmRegistered) {
|
|
401
|
+
this.conceptMapProviders.push(providerSet.conceptMapProvider);
|
|
402
|
+
providerSet.cmRegistered = true;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
286
407
|
async loadInternal(details, isDefault, mode) {
|
|
287
408
|
if (isDefault) {
|
|
288
409
|
throw new Error("Default is not supported for internal code system providers");
|
|
@@ -464,13 +585,7 @@ class Library {
|
|
|
464
585
|
for (const resource of resources) {
|
|
465
586
|
const cs = new CodeSystem(await contentLoader.loadFile(resource, contentLoader.fhirVersion()));
|
|
466
587
|
cs.sourcePackage = contentLoader.pid();
|
|
467
|
-
|
|
468
|
-
if (!existing || cs.isMoreRecent(existing)) {
|
|
469
|
-
cp.codeSystems.set(cs.url, cs);
|
|
470
|
-
}
|
|
471
|
-
if (cs.version) {
|
|
472
|
-
cp.codeSystems.set(cs.vurl, cs);
|
|
473
|
-
}
|
|
588
|
+
cp.codeSystems.push(cs);
|
|
474
589
|
csc++;
|
|
475
590
|
}
|
|
476
591
|
this.codeSystemProviders.push(cp);
|
|
@@ -686,10 +801,12 @@ class Library {
|
|
|
686
801
|
}
|
|
687
802
|
|
|
688
803
|
|
|
804
|
+
provider.codeSystemProviders = this.codeSystemProviders;
|
|
805
|
+
provider.context = context;
|
|
689
806
|
for (const cp of this.codeSystemProviders) {
|
|
690
|
-
const
|
|
691
|
-
for (const
|
|
692
|
-
provider.
|
|
807
|
+
const csList = await cp.listCodeSystems(fhirVersion, context);
|
|
808
|
+
for (const cs of csList) {
|
|
809
|
+
provider.addCodeSystem(cs);
|
|
693
810
|
}
|
|
694
811
|
}
|
|
695
812
|
// Don't clone valueSetProviders yet - we'll build it with correct order
|
package/tx/ocl/README.md
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
# OCL Integration in FHIRsmith
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
The `tx/ocl` module integrates [Open Concept Lab (OCL)](https://openconceptlab.org/) as a terminology source inside FHIRsmith.
|
|
5
|
+
|
|
6
|
+
It provides adapters/providers for:
|
|
7
|
+
|
|
8
|
+
- `CodeSystem` from OCL Sources
|
|
9
|
+
- `ValueSet` from OCL Collections
|
|
10
|
+
- `ConceptMap` from OCL Mappings
|
|
11
|
+
|
|
12
|
+
In FHIRsmith terms, these providers are loaded by `tx/library.js` when a source entry starts with `ocl:`. OCL metadata is discovered from OCL APIs, and heavy content (concept lists/expansions) is loaded lazily and warmed in background jobs.
|
|
13
|
+
|
|
14
|
+
## Architecture
|
|
15
|
+
### Main modules
|
|
16
|
+
- `cs-ocl.js`
|
|
17
|
+
- `OCLCodeSystemProvider`: discovers OCL sources and publishes CodeSystem metadata.
|
|
18
|
+
- `OCLSourceCodeSystemFactory`: creates runtime CodeSystem providers with shared caches, cold-cache hydration, and background full-load jobs.
|
|
19
|
+
- `OCLSourceCodeSystemProvider`: runtime concept lookup/filter/search behavior for one source.
|
|
20
|
+
- `vs-ocl.js`
|
|
21
|
+
- `OCLValueSetProvider`: discovers OCL collections, resolves compose includes, serves ValueSet metadata, and builds cached expansions in background.
|
|
22
|
+
- `cm-ocl.js`
|
|
23
|
+
- `OCLConceptMapProvider`: resolves OCL mappings for fetch/search/translation candidate discovery.
|
|
24
|
+
|
|
25
|
+
### Supporting modules under `tx/ocl`
|
|
26
|
+
- `http/client.js`: Axios client creation, base URL normalization, token auth header (`Token` or `Bearer`).
|
|
27
|
+
- `http/pagination.js`: OCL pagination helper (`results/items/data`, `next`, page mode fallback).
|
|
28
|
+
- `cache/cache-paths.js`: cold-cache directories and canonical URL to file path mapping.
|
|
29
|
+
- `cache/cache-utils.js`: cache directory creation, file age detection, friendly age formatting.
|
|
30
|
+
- `fingerprint/fingerprint.js`: deterministic SHA-256 fingerprints for CodeSystem concepts and ValueSet expansions.
|
|
31
|
+
- `jobs/background-queue.js`: singleton keyed queue, size-priority ordering, max concurrency = 2, heartbeat logging every 30s.
|
|
32
|
+
- `mappers/concept-mapper.js`: maps OCL concept payloads to internal concept context shape.
|
|
33
|
+
- `model/concept-filter-context.js`: ranked filter result set used by CodeSystem filters.
|
|
34
|
+
- `shared/constants.js`: defaults and constants (`PAGE_SIZE`, cache freshness window, etc.).
|
|
35
|
+
- `shared/patches.js`:
|
|
36
|
+
- patches search worker so `CodeSystem?url=...&code=...` on OCL resources only returns matching concept subtree.
|
|
37
|
+
- patches `TxParameters.hashSource()` to include `filter` for expansion cache key differentiation.
|
|
38
|
+
|
|
39
|
+
## Runtime flow
|
|
40
|
+
### Metadata discovery
|
|
41
|
+
CodeSystems (`cs-ocl.js`):
|
|
42
|
+
- discover orgs via `/orgs/`
|
|
43
|
+
- for each org, discover sources via `/orgs/{org}/sources/`
|
|
44
|
+
- fallback to `/sources/` if org listing is unavailable
|
|
45
|
+
|
|
46
|
+
ValueSets (`vs-ocl.js`):
|
|
47
|
+
- discover orgs via `/orgs/`
|
|
48
|
+
- discover collections via `/orgs/{org}/collections/`
|
|
49
|
+
- fallback to `/collections/`
|
|
50
|
+
|
|
51
|
+
ConceptMaps (`cm-ocl.js`):
|
|
52
|
+
- fetch by id via `/mappings/{id}/`
|
|
53
|
+
- search via `/mappings/` or `/orgs/{org}/mappings/`
|
|
54
|
+
|
|
55
|
+
### Lazy loading
|
|
56
|
+
- CodeSystem concepts are not fully loaded at metadata discovery time.
|
|
57
|
+
- ValueSet expansions are not built inline by default.
|
|
58
|
+
- Missing concept/page/expansion data triggers background warm-up scheduling.
|
|
59
|
+
|
|
60
|
+
### Cold cache (disk)
|
|
61
|
+
- Base folder: `data/terminology-cache/ocl`
|
|
62
|
+
- CodeSystems: `data/terminology-cache/ocl/codesystems`
|
|
63
|
+
- ValueSets: `data/terminology-cache/ocl/valuesets`
|
|
64
|
+
|
|
65
|
+
On startup/initialization:
|
|
66
|
+
- CodeSystem factory hydrates concept cache from cold cache file if present.
|
|
67
|
+
- ValueSet provider loads cached expansions from cold cache files.
|
|
68
|
+
|
|
69
|
+
Corrupt cache handling:
|
|
70
|
+
- JSON parse/read errors are logged and skipped; provider continues without crashing.
|
|
71
|
+
|
|
72
|
+
### Hot cache (memory)
|
|
73
|
+
- CodeSystems: shared in-memory concept/page caches per factory.
|
|
74
|
+
- ValueSets: in-memory expansion cache keyed by ValueSet + params hash.
|
|
75
|
+
- ConceptMaps: in-memory map keyed by URL/version/id.
|
|
76
|
+
|
|
77
|
+
### Background warm-up jobs
|
|
78
|
+
Queue behavior (`OCLBackgroundJobQueue`):
|
|
79
|
+
- singleton job key (skip duplicate key)
|
|
80
|
+
- max concurrency = `2`
|
|
81
|
+
- ordering by `jobSize` (smaller concept count first)
|
|
82
|
+
- heartbeat log every `30s`
|
|
83
|
+
- progress supports `{ processed, total }` and `%`
|
|
84
|
+
|
|
85
|
+
CodeSystem warm-up:
|
|
86
|
+
- skipped when cold-cache file age is `<= 1 hour`
|
|
87
|
+
- enqueued when stale/no cold cache
|
|
88
|
+
- loads all concept pages
|
|
89
|
+
- computes fingerprint from full concept content
|
|
90
|
+
- if fingerprint changed, replaces cold cache file
|
|
91
|
+
|
|
92
|
+
ValueSet warm-up:
|
|
93
|
+
- skipped when freshest cold cache age (file mtime or cached timestamp) is `<= 1 hour`
|
|
94
|
+
- enqueued when stale/no cold cache
|
|
95
|
+
- builds expansion by paging collection concepts
|
|
96
|
+
- computes expansion fingerprint
|
|
97
|
+
- if fingerprint changed, replaces cold cache file
|
|
98
|
+
|
|
99
|
+
### Fingerprint/checksum strategy
|
|
100
|
+
- OCL source checksum is treated as informational only in `cs-ocl.js` comments.
|
|
101
|
+
- Cache replacement decisions use custom fingerprints from concept/expansion content.
|
|
102
|
+
- ValueSet cache validity also checks metadata signature and dependency checksums from referenced CodeSystems.
|
|
103
|
+
|
|
104
|
+
### ValueSet expansion filtering
|
|
105
|
+
`vs-ocl.js` supports filtered concept retrieval through `valueSet.oclFetchConcepts(...)`:
|
|
106
|
+
- local baseline filtering (code/display/definition text contains)
|
|
107
|
+
- `SearchFilterText` token behavior
|
|
108
|
+
- remote query hint (`q`) generated from normalized filter tokens
|
|
109
|
+
- dedicated smaller page size for filtered calls (`FILTERED_CONCEPT_PAGE_SIZE`)
|
|
110
|
+
|
|
111
|
+
### `/CodeSystem?url=...&code=...` behavior for OCL
|
|
112
|
+
`shared/patches.js` patches search worker to apply concept subtree filtering only for OCL-marked CodeSystems (extension URL `http://fhir.org/FHIRsmith/StructureDefinition/ocl-codesystem`).
|
|
113
|
+
|
|
114
|
+
## Configuration
|
|
115
|
+
## Activation in FHIRsmith
|
|
116
|
+
OCL is activated by adding an OCL source line in the TX library YAML (for example `data/library.yml`):
|
|
117
|
+
|
|
118
|
+
```yaml
|
|
119
|
+
sources:
|
|
120
|
+
- ocl:https://oclapi2.ips.hsl.org.br
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`tx/library.js` loads this via `loadOcl()`.
|
|
124
|
+
|
|
125
|
+
### Source syntax
|
|
126
|
+
`tx/library.js` parses:
|
|
127
|
+
|
|
128
|
+
```text
|
|
129
|
+
ocl:<baseUrl>|org=<orgId>|token=<tokenOrAlias>|timeout=<ms>
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Parsed keys:
|
|
133
|
+
- `baseUrl` (required)
|
|
134
|
+
- `org` (optional)
|
|
135
|
+
- `token` (optional)
|
|
136
|
+
- `timeout` (optional positive number)
|
|
137
|
+
|
|
138
|
+
Examples:
|
|
139
|
+
|
|
140
|
+
```yaml
|
|
141
|
+
sources:
|
|
142
|
+
- ocl:https://api.openconceptlab.org
|
|
143
|
+
- ocl:https://ocl.example.org|org=my-org
|
|
144
|
+
- ocl:https://ocl.example.org|org=my-org|token=my-ocl-token|timeout=45000
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Config value aliasing
|
|
148
|
+
`Library.resolveOclConfigValue()` can resolve symbolic values from top-level YAML `ocl:` object loaded into `this.oclConfig`.
|
|
149
|
+
|
|
150
|
+
Example pattern:
|
|
151
|
+
|
|
152
|
+
```yaml
|
|
153
|
+
ocl:
|
|
154
|
+
ocl-base: https://ocl.example.org
|
|
155
|
+
ocl-token: Token abc123
|
|
156
|
+
|
|
157
|
+
sources:
|
|
158
|
+
- ocl:ocl-base|org=my-org|token=ocl-token
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Credentials and URLs
|
|
162
|
+
- Base URL is required (OCL API root).
|
|
163
|
+
- Token is optional, sent as `Authorization`:
|
|
164
|
+
- `Token <value>` if no prefix is provided
|
|
165
|
+
- preserved if already `Token ...` or `Bearer ...`
|
|
166
|
+
|
|
167
|
+
### Enable/disable integration
|
|
168
|
+
- Enable by including at least one `ocl:` source entry in TX library YAML.
|
|
169
|
+
- Disable by removing/commenting those `ocl:` entries.
|
|
170
|
+
- `modules.tx.enabled` must be true in server config to expose TX endpoints.
|
|
171
|
+
|
|
172
|
+
## Cache behavior details
|
|
173
|
+
### Startup hydration
|
|
174
|
+
- CodeSystem cold cache is loaded when factory is created (`OCLSourceCodeSystemFactory` constructor path).
|
|
175
|
+
- ValueSet cold cache is loaded during `OCLValueSetProvider.initialize()`.
|
|
176
|
+
|
|
177
|
+
### 1-hour freshness rule
|
|
178
|
+
- Fresh threshold constant: `COLD_CACHE_FRESHNESS_MS = 60 * 60 * 1000`.
|
|
179
|
+
- If cold cache age is `<= 1 hour`, warm-up scheduling is skipped.
|
|
180
|
+
|
|
181
|
+
### Hot cache replacing cold cache
|
|
182
|
+
- On successful background refresh, new fingerprint is compared to previous cold-cache fingerprint.
|
|
183
|
+
- If changed, cold cache is overwritten with the refreshed in-memory state.
|
|
184
|
+
|
|
185
|
+
## Operational notes
|
|
186
|
+
### Logs to expect
|
|
187
|
+
Prefixes:
|
|
188
|
+
- `[OCL]` for CodeSystem/queue/general OCL flow
|
|
189
|
+
- `[OCL-ValueSet]` for ValueSet flow
|
|
190
|
+
|
|
191
|
+
Typical events:
|
|
192
|
+
- fetched org/source/collection counts
|
|
193
|
+
- cold cache loaded/saved
|
|
194
|
+
- warm-up skipped/enqueued/started/completed
|
|
195
|
+
- fingerprint unchanged/changed
|
|
196
|
+
- queue status + heartbeat snapshots
|
|
197
|
+
|
|
198
|
+
### Troubleshooting hints
|
|
199
|
+
- If nothing loads, verify OCL base URL and org visibility.
|
|
200
|
+
- If cache never warms, check cold-cache timestamps and 1-hour rule.
|
|
201
|
+
- If searches by `code` behave unexpectedly, confirm OCL marker extension is present on CodeSystem resources.
|
|
202
|
+
- For ValueSet filter behavior, verify `filter` is included in request path through `TxParameters.hashSource` patch and that filter text normalizes as expected.
|
|
203
|
+
|
|
204
|
+
### Known limitations (from current implementation)
|
|
205
|
+
- OCL checksums are not relied on for cache invalidation.
|
|
206
|
+
- Some source/collection discovery paths depend on OCL endpoint support and can fallback.
|
|
207
|
+
- Missing endpoints returning `404` in some concept fetch paths are treated as empty content for graceful degradation.
|
|
208
|
+
|
|
209
|
+
## Developer notes
|
|
210
|
+
### Where to extend
|
|
211
|
+
- Add/adjust OCL HTTP behavior: `tx/ocl/http/*`
|
|
212
|
+
- Add cache policy/path behavior: `tx/ocl/cache/*`
|
|
213
|
+
- Add queue policy: `tx/ocl/jobs/background-queue.js`
|
|
214
|
+
- Add mapping logic from OCL payloads: `tx/ocl/mappers/*`
|
|
215
|
+
- Add fingerprint strategy: `tx/ocl/fingerprint/*`
|
|
216
|
+
- Add provider-specific behavior:
|
|
217
|
+
- CodeSystem: `tx/ocl/cs-ocl.js`
|
|
218
|
+
- ValueSet: `tx/ocl/vs-ocl.js`
|
|
219
|
+
- ConceptMap: `tx/ocl/cm-ocl.js`
|
|
220
|
+
|
|
221
|
+
### Ownership by concern
|
|
222
|
+
- HTTP access: `http/client.js`, `http/pagination.js`
|
|
223
|
+
- Disk cold cache and pathing: `cache/cache-paths.js`, `cache/cache-utils.js`
|
|
224
|
+
- Background jobs and scheduling policy: `jobs/background-queue.js`
|
|
225
|
+
- Fingerprints/checksums: `fingerprint/fingerprint.js` and provider compare logic
|
|
226
|
+
- Worker/runtime patches: `shared/patches.js`
|
|
227
|
+
|
|
228
|
+
### Test strategy for future changes
|
|
229
|
+
- Keep provider tests deterministic by mocking OCL HTTP responses and cold-cache files.
|
|
230
|
+
- Validate stale/fresh transitions with file mtime control.
|
|
231
|
+
- Validate queue behavior with isolated static state reset between tests.
|
|
232
|
+
- Track coverage with:
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
npm test -- --runInBand tests/ocl --coverage --collectCoverageFrom="tx/ocl/**/*.js"
|
|
236
|
+
```
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
|
|
3
|
+
const CACHE_BASE_DIR = path.join(process.cwd(), 'data', 'terminology-cache', 'ocl');
|
|
4
|
+
const CACHE_CS_DIR = path.join(CACHE_BASE_DIR, 'codesystems');
|
|
5
|
+
const CACHE_VS_DIR = path.join(CACHE_BASE_DIR, 'valuesets');
|
|
6
|
+
|
|
7
|
+
function sanitizeFilename(text) {
|
|
8
|
+
if (!text || typeof text !== 'string') {
|
|
9
|
+
return 'unknown';
|
|
10
|
+
}
|
|
11
|
+
return text
|
|
12
|
+
.replace(/[^a-zA-Z0-9._-]/g, '_')
|
|
13
|
+
.replace(/_+/g, '_')
|
|
14
|
+
.substring(0, 200);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function getCacheFilePath(baseDir, canonicalUrl, version = null, paramsKey = null) {
|
|
18
|
+
const filename = sanitizeFilename(canonicalUrl)
|
|
19
|
+
+ (version ? `_${sanitizeFilename(version)}` : '')
|
|
20
|
+
+ (paramsKey && paramsKey !== 'default' ? `_p_${sanitizeFilename(paramsKey)}` : '')
|
|
21
|
+
+ '.json';
|
|
22
|
+
|
|
23
|
+
return path.join(baseDir, filename);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = {
|
|
27
|
+
CACHE_BASE_DIR,
|
|
28
|
+
CACHE_CS_DIR,
|
|
29
|
+
CACHE_VS_DIR,
|
|
30
|
+
sanitizeFilename,
|
|
31
|
+
getCacheFilePath
|
|
32
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
const fs = require('fs/promises');
|
|
2
|
+
const fsSync = require('fs');
|
|
3
|
+
|
|
4
|
+
async function ensureCacheDirectories(...dirs) {
|
|
5
|
+
for (const dir of dirs) {
|
|
6
|
+
if (!dir) {
|
|
7
|
+
continue;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
await fs.mkdir(dir, { recursive: true });
|
|
12
|
+
} catch (error) {
|
|
13
|
+
console.error('[OCL] Failed to create cache directory:', dir, error.message);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function getColdCacheAgeMs(cacheFilePath, logPrefix = '[OCL]') {
|
|
19
|
+
try {
|
|
20
|
+
const stats = fsSync.statSync(cacheFilePath);
|
|
21
|
+
if (!stats || !Number.isFinite(stats.mtimeMs)) {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return Math.max(0, Date.now() - stats.mtimeMs);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
if (error && error.code !== 'ENOENT') {
|
|
28
|
+
console.error(`${logPrefix} Failed to inspect cold cache file ${cacheFilePath}: ${error.message}`);
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function formatCacheAgeMinutes(ageMs) {
|
|
35
|
+
const minutes = Math.max(1, Math.round(ageMs / 60000));
|
|
36
|
+
return `${minutes} minute${minutes === 1 ? '' : 's'}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
module.exports = {
|
|
40
|
+
ensureCacheDirectories,
|
|
41
|
+
getColdCacheAgeMs,
|
|
42
|
+
formatCacheAgeMinutes
|
|
43
|
+
};
|