fhirsmith 0.14.2 → 0.15.1
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 +56 -1
- package/config-template.json +10 -0
- package/library/changelog.js +57 -0
- package/library/npm-audit.js +62 -9
- package/package.json +2 -2
- package/packages/npm-check-page.js +108 -0
- package/packages/npm-check.js +450 -0
- package/packages/openapi.yaml +158 -5
- package/packages/package-crawler.js +203 -20
- package/packages/packages.js +250 -8
- package/packages/readme.md +55 -2
- package/packages/utilities/dedupe-versions.js +242 -0
- package/registry/crawler.js +17 -0
- package/registry/fhirsmith-releases.js +3 -6
- package/registry/model.js +3 -0
- package/registry/registry.js +28 -3
- package/translations/Messages.properties +3 -0
- package/tx/README.md +20 -4
- package/tx/cm/cm-api.js +25 -10
- package/tx/cm/cm-database.js +0 -4
- package/tx/cm/cm-package.js +53 -14
- package/tx/cs/cs-api.js +6 -1
- package/tx/cs/cs-icd11.js +23 -6
- package/tx/cs/cs-provider-api.js +22 -7
- package/tx/cs/cs-provider-list.js +10 -9
- package/tx/cs/icd11-expressions.js +5 -6
- package/tx/importers/icd11-schema.md +1 -0
- package/tx/importers/import-icd11.js +39 -0
- package/tx/library/resource-ids.js +107 -0
- package/tx/library.js +96 -24
- package/tx/ocl/cm-ocl.cjs +23 -15
- package/tx/ocl/cs-ocl.cjs +9 -10
- package/tx/ocl/vs-ocl.cjs +17 -17
- package/tx/provider.js +80 -13
- package/tx/tx.fhir.org.yml +17 -9
- package/tx/tx.js +7 -1
- package/tx/vs/vs-api.js +25 -10
- package/tx/vs/vs-database.js +0 -4
- package/tx/vs/vs-package.js +48 -31
- package/tx/vs/vs-vsac.js +20 -5
- package/tx/workers/lookup.js +7 -4
- package/tx/workers/metadata.js +2 -1
- package/tx/workers/read.js +3 -18
package/tx/cs/cs-icd11.js
CHANGED
|
@@ -113,8 +113,11 @@ class ICD11Services extends BaseCSServices {
|
|
|
113
113
|
/**
|
|
114
114
|
* The default is system|version, but $lookup's `name` is meant to be a display name for
|
|
115
115
|
* the code system. OMOP overrides it the same way; WHO returns the linearization title.
|
|
116
|
+
* It follows the request's languages like any other display, falling back to the primary
|
|
117
|
+
* title. $lookup passes its working languages (displayLanguage, else Accept-Language),
|
|
118
|
+
* since displayLanguage never reaches opContext.langs; other callers pass nothing.
|
|
116
119
|
*/
|
|
117
|
-
name() { return this.shared.title; }
|
|
120
|
+
name(languages) { return this.shared.titles?.[this.#displayLang(languages)] || this.shared.title; }
|
|
118
121
|
description() { return this.shared.title; }
|
|
119
122
|
totalCount() { return this.shared.count; }
|
|
120
123
|
defLang() { return 'en'; }
|
|
@@ -191,12 +194,13 @@ class ICD11Services extends BaseCSServices {
|
|
|
191
194
|
return row ? new ICD11Concept(row) : null;
|
|
192
195
|
}
|
|
193
196
|
|
|
194
|
-
/**
|
|
197
|
+
/** What the expression parser needs: the database, and the request's translations. */
|
|
195
198
|
#expressionContext() {
|
|
196
199
|
return {
|
|
197
200
|
get: (sql, params) => this.#get(sql, params),
|
|
198
201
|
all: (sql, params) => this.#all(sql, params),
|
|
199
202
|
resolve: (text) => this.#resolve(text),
|
|
203
|
+
translate: (msgId, params) => this.opContext.i18n.translate(msgId, this.opContext.langs, params),
|
|
200
204
|
};
|
|
201
205
|
}
|
|
202
206
|
|
|
@@ -309,8 +313,8 @@ class ICD11Services extends BaseCSServices {
|
|
|
309
313
|
* iterable of Language, in quality order; the database holds translations keyed on the
|
|
310
314
|
* primary subtag, so that is what is matched.
|
|
311
315
|
*/
|
|
312
|
-
#displayLang() {
|
|
313
|
-
const langs = this.opContext?.langs;
|
|
316
|
+
#displayLang(languages) {
|
|
317
|
+
const langs = languages || this.opContext?.langs;
|
|
314
318
|
if (langs) {
|
|
315
319
|
for (const l of langs) {
|
|
316
320
|
const base = l.language || String(l.code || '').split('-')[0];
|
|
@@ -615,8 +619,9 @@ class ICD11Services extends BaseCSServices {
|
|
|
615
619
|
if (!target) {
|
|
616
620
|
// a value set that names a code that is not there is broken, and saying so is more
|
|
617
621
|
// use than quietly expanding to nothing
|
|
618
|
-
throw new Issue('error', 'code-invalid', null,
|
|
619
|
-
|
|
622
|
+
throw new Issue('error', 'code-invalid', null, 'FILTER_VALUE_UNKNOWN_CODE',
|
|
623
|
+
this.opContext.i18n.translate('FILTER_VALUE_UNKNOWN_CODE', this.opContext.langs, [value, this.system()]),
|
|
624
|
+
'invalid-code', 422);
|
|
620
625
|
}
|
|
621
626
|
filterContext.filters.push(new ICD11FilterSet('isa', {
|
|
622
627
|
ancestorId: target.id, minDepth: op === 'is-a' ? 0 : 1, value,
|
|
@@ -749,12 +754,24 @@ class ICD11ServicesFactory extends CodeSystemFactoryProvider {
|
|
|
749
754
|
}
|
|
750
755
|
const count = await get('SELECT COUNT(*) n FROM concept WHERE system = ?', [sys.id]);
|
|
751
756
|
const langs = await get("SELECT value FROM meta WHERE key = 'languages'", []);
|
|
757
|
+
// per-language titles; a database built before system_title existed has none
|
|
758
|
+
const all = (sql, p) => new Promise((res, rej) =>
|
|
759
|
+
db.all(sql, p, (e, r) => e ? rej(e) : res(r || [])));
|
|
760
|
+
const titles = {};
|
|
761
|
+
const hasTitles = await get(
|
|
762
|
+
"SELECT 1 x FROM sqlite_master WHERE type = 'table' AND name = 'system_title'", []);
|
|
763
|
+
if (hasTitles) {
|
|
764
|
+
for (const r of await all('SELECT lang, title FROM system_title WHERE system = ?', [sys.id])) {
|
|
765
|
+
titles[r.lang] = r.title;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
752
768
|
this._shared = {
|
|
753
769
|
systemCode: sys.code,
|
|
754
770
|
systemId: sys.id,
|
|
755
771
|
url: sys.url,
|
|
756
772
|
version: sys.version,
|
|
757
773
|
title: sys.title,
|
|
774
|
+
titles,
|
|
758
775
|
isTree: !!sys.is_tree,
|
|
759
776
|
count: count.n,
|
|
760
777
|
languages: (langs?.value || 'en').split(','),
|
package/tx/cs/cs-provider-api.js
CHANGED
|
@@ -1,22 +1,37 @@
|
|
|
1
|
+
const { assignIdSpace } = require('../library/resource-ids');
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Abstract base class for value set providers
|
|
3
5
|
* Defines the interface that all value set providers must implement
|
|
4
6
|
*/
|
|
5
7
|
class AbstractCodeSystemProvider {
|
|
6
8
|
/**
|
|
7
|
-
* {
|
|
9
|
+
* {string} The prefix of this provider's id space (set by assignIds) - given for its source in the library
|
|
10
|
+
* YAML, or autonumbered. Every code system this provider provides has an id that starts
|
|
11
|
+
* with spaceId + '-' (see tx/library/resource-ids.js)
|
|
8
12
|
*/
|
|
9
13
|
spaceId;
|
|
10
14
|
|
|
11
15
|
/**
|
|
12
|
-
*
|
|
13
|
-
* in
|
|
16
|
+
* Put this provider in an id space: set spaceId, and prefix the ids of the code systems it
|
|
17
|
+
* provides with spaceId + '-'. A provider is put in one id space, once - asking for a
|
|
18
|
+
* different one later is an error (see assignIdSpace)
|
|
14
19
|
*
|
|
15
|
-
* @param {
|
|
20
|
+
* @param {string} spaceId
|
|
16
21
|
*/
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
22
|
+
assignIds(spaceId) {
|
|
23
|
+
if (assignIdSpace(this, spaceId)) {
|
|
24
|
+
this.prefixIds();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Prefix the ids of the code systems this provider provides with spaceId + '-', once spaceId
|
|
30
|
+
* is set (by assignIds). Code systems the provider provides later (see
|
|
31
|
+
* getCodeSystemChanges) must be given ids in the same space.
|
|
32
|
+
*/
|
|
33
|
+
prefixIds() {
|
|
34
|
+
throw new Error('prefixIds must be implemented by subclass');
|
|
20
35
|
}
|
|
21
36
|
|
|
22
37
|
/**
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const { AbstractCodeSystemProvider } = require('./cs-provider-api');
|
|
2
|
+
const { applyIdPrefix } = require('../library/resource-ids');
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Package-based ValueSet provider using shared database layer
|
|
@@ -11,18 +12,18 @@ class ListCodeSystemProvider extends AbstractCodeSystemProvider {
|
|
|
11
12
|
codeSystems = [];
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
|
-
*
|
|
15
|
-
* in the
|
|
16
|
-
*
|
|
17
|
-
* @param {Set<String>} ids
|
|
15
|
+
* prefix the ids of the code systems with this provider's spaceId. A code system without
|
|
16
|
+
* an id gets its position in the list
|
|
18
17
|
*/
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
prefixIds() {
|
|
19
|
+
let i = 0;
|
|
21
20
|
for (const cs of this.codeSystems) {
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
i++;
|
|
22
|
+
const json = cs.jsonObj || cs;
|
|
23
|
+
if (!json.id) {
|
|
24
|
+
json.id = String(i);
|
|
24
25
|
}
|
|
25
|
-
|
|
26
|
+
applyIdPrefix(cs, this.spaceId);
|
|
26
27
|
}
|
|
27
28
|
}
|
|
28
29
|
|
|
@@ -232,12 +232,11 @@ async function parseExpression(ctx, text) {
|
|
|
232
232
|
*/
|
|
233
233
|
function notOnAnyAxis(ctx, stem, token) {
|
|
234
234
|
const axes = stem.axes.map(a => a.axis_name).join(', ');
|
|
235
|
-
const
|
|
236
|
-
?
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
return new Issue('error', 'code-invalid', null, null, msg, 'invalid-code', 400).handleAsOO(400);
|
|
235
|
+
const [msgId, params] = axes
|
|
236
|
+
? ['ICD11_PC_VALUE_NOT_ON_AXIS', [token.text, stem.stem.text, axes]]
|
|
237
|
+
: ['ICD11_PC_NO_AXES', [token.text, stem.stem.text]];
|
|
238
|
+
return new Issue('error', 'code-invalid', null, msgId, ctx.translate(msgId, params),
|
|
239
|
+
'invalid-code', 400).handleAsOO(400);
|
|
241
240
|
}
|
|
242
241
|
|
|
243
242
|
module.exports = { ICD11Expression, ExpressionToken, parseExpression, tokenize, SCHEMA_NS };
|
|
@@ -42,6 +42,7 @@ text lives in `designation`. The untouched native JSON for each concept is kept
|
|
|
42
42
|
|---|---|
|
|
43
43
|
| `meta` | key/value: release, languages, endpoints, tool version, request count |
|
|
44
44
|
| `system` | one row per code system — `mms`, `icf`, `foundation` — with its canonical url, release and `is_tree` |
|
|
45
|
+
| `system_title` | the code system's title per language (`system.title` is the primary one); only titles WHO actually translated |
|
|
45
46
|
| `concept` | one row per entity |
|
|
46
47
|
| `concept_parent` | parent/child edges, `seq` preserving the API's order |
|
|
47
48
|
| `concept_closure` | transitive closure including the reflexive row, so is-a never needs recursion |
|
|
@@ -55,6 +55,15 @@ CREATE TABLE IF NOT EXISTS system (
|
|
|
55
55
|
uri_stem TEXT NOT NULL -- versioned uri prefix used by the native API
|
|
56
56
|
);
|
|
57
57
|
|
|
58
|
+
-- the code system's own title, per language crawled (system.title is the primary language).
|
|
59
|
+
-- Only a title WHO actually translated is recorded -- an English fallback is not a French title.
|
|
60
|
+
CREATE TABLE IF NOT EXISTS system_title (
|
|
61
|
+
system INTEGER NOT NULL REFERENCES system(id),
|
|
62
|
+
lang TEXT NOT NULL,
|
|
63
|
+
title TEXT NOT NULL,
|
|
64
|
+
PRIMARY KEY (system, lang)
|
|
65
|
+
);
|
|
66
|
+
|
|
58
67
|
-- one row per concept
|
|
59
68
|
CREATE TABLE IF NOT EXISTS concept (
|
|
60
69
|
id INTEGER PRIMARY KEY,
|
|
@@ -374,6 +383,8 @@ class Icd11Importer {
|
|
|
374
383
|
setMeta: db.prepare(`INSERT INTO meta(key,value) VALUES (?,?)
|
|
375
384
|
ON CONFLICT(key) DO UPDATE SET value=excluded.value`),
|
|
376
385
|
getMeta: db.prepare(`SELECT value FROM meta WHERE key=?`),
|
|
386
|
+
insSystemTitle: db.prepare(`INSERT INTO system_title(system,lang,title) VALUES (?,?,?)
|
|
387
|
+
ON CONFLICT(system,lang) DO UPDATE SET title=excluded.title`),
|
|
377
388
|
insSystem: db.prepare(`INSERT INTO system(code,url,version,title,is_tree,api_root,uri_stem)
|
|
378
389
|
VALUES (@code,@url,@version,@title,@is_tree,@api_root,@uri_stem)
|
|
379
390
|
ON CONFLICT(code) DO UPDATE SET
|
|
@@ -566,6 +577,8 @@ class Icd11Importer {
|
|
|
566
577
|
|
|
567
578
|
for (const lang of languages) {
|
|
568
579
|
const isPrimary = lang === primaryLang;
|
|
580
|
+
// before the resume check: a language whose concepts are all done still needs its title
|
|
581
|
+
await this.storeSystemTitle(systemId, def, lang, isPrimary ? root : null);
|
|
569
582
|
const done = new Set(this.stmt.doneLangs.all(systemId, lang).map(r => r.uri));
|
|
570
583
|
const todo = uris.filter(u => !done.has(unversion(u)));
|
|
571
584
|
if (!todo.length) { this.log(` [${lang}] already complete`); continue; }
|
|
@@ -609,6 +622,32 @@ class Icd11Importer {
|
|
|
609
622
|
return systemId;
|
|
610
623
|
}
|
|
611
624
|
|
|
625
|
+
/**
|
|
626
|
+
* Record the linearization root's title in one language. `root` is the document already
|
|
627
|
+
* fetched for the primary language; other languages cost one extra call each. The root's
|
|
628
|
+
* title carries the language WHO actually answered in, so an untranslated title (WHO falls
|
|
629
|
+
* back to English) is skipped rather than stored under the requested language.
|
|
630
|
+
*/
|
|
631
|
+
async storeSystemTitle(systemId, def, lang, root) {
|
|
632
|
+
if (!root) {
|
|
633
|
+
const r = await this.getEntity(WHO + def.apiRoot, lang);
|
|
634
|
+
if (!r.ok) {
|
|
635
|
+
this.problem('title-failed', r.error, { system: def.code, lang, uri: def.apiRoot });
|
|
636
|
+
this.warn(` ! could not read the ${lang} title of ${def.apiRoot}: ${r.error}`);
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
root = r.body;
|
|
640
|
+
}
|
|
641
|
+
const title = textOf(root.title);
|
|
642
|
+
if (!title) return;
|
|
643
|
+
const got = langOf(root.title, lang);
|
|
644
|
+
if (got.split('-')[0] !== lang.split('-')[0]) {
|
|
645
|
+
this.log(` [${lang}] no translated title (WHO answered in ${got})`);
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
this.stmt.insSystemTitle.run(systemId, lang, title);
|
|
649
|
+
}
|
|
650
|
+
|
|
612
651
|
/**
|
|
613
652
|
* Turn the parent and scale-entity uris collected during the crawl into real ids.
|
|
614
653
|
* `rootUri` is the linearization root, which the top-level concepts name as their
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Resource ids in the server's id spaces
|
|
3
|
+
//
|
|
4
|
+
// Every CodeSystem, ValueSet and ConceptMap the server serves by id lives in the id space of
|
|
5
|
+
// the provider that loaded it, and its id is the provider's prefix, a '-', then the id the
|
|
6
|
+
// resource had in its source: "tho-v3-ActCode", "core-v3-MaritalStatus". The prefix is the
|
|
7
|
+
// provider's spaceId: given in the library YAML (npm:hl7.terminology=tho), autonumbered by
|
|
8
|
+
// the library when the YAML doesn't give one, and "core" for the FHIR core package each
|
|
9
|
+
// endpoint loads for itself.
|
|
10
|
+
//
|
|
11
|
+
// No resource is served under its original id. Two sources can carry resources with the same
|
|
12
|
+
// id (hl7.terminology and hl7.fhir.r4.core both have a CodeSystem v3-MaritalStatus), so an
|
|
13
|
+
// unprefixed id can't say which one it means - and anything that looks a resource up by its
|
|
14
|
+
// raw id must fail, rather than find whichever one happens to come first.
|
|
15
|
+
//
|
|
16
|
+
|
|
17
|
+
// resources whose id has been prefixed - so prefixing is idempotent, and a raw id that
|
|
18
|
+
// happens to start with the prefix still gets one
|
|
19
|
+
const prefixed = new WeakSet();
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Prefix a resource's id with spaceId. The resource is either a wrapper with a jsonObj
|
|
23
|
+
* (CodeSystem, ValueSet, ConceptMap) or a plain json object. Does nothing to a resource
|
|
24
|
+
* that has already been prefixed, or that has no id.
|
|
25
|
+
*
|
|
26
|
+
* @param {Object} resource
|
|
27
|
+
* @param {string} spaceId
|
|
28
|
+
*/
|
|
29
|
+
function applyIdPrefix(resource, spaceId) {
|
|
30
|
+
if (!resource || !spaceId || prefixed.has(resource)) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const json = resource.jsonObj || resource;
|
|
34
|
+
if (!json.id) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const id = `${spaceId}-${json.id}`;
|
|
38
|
+
json.id = id;
|
|
39
|
+
if (resource.jsonObj && Object.prototype.hasOwnProperty.call(resource, 'id')) {
|
|
40
|
+
resource.id = id;
|
|
41
|
+
}
|
|
42
|
+
prefixed.add(resource);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The id a resource had in its source, for an id served in spaceId - or null, if the id
|
|
47
|
+
* isn't in that space (including when it's a raw id, or there's no spaceId).
|
|
48
|
+
*
|
|
49
|
+
* @param {string} id
|
|
50
|
+
* @param {string} spaceId
|
|
51
|
+
* @returns {string|null}
|
|
52
|
+
*/
|
|
53
|
+
function stripIdPrefix(id, spaceId) {
|
|
54
|
+
if (!spaceId || typeof id !== 'string') {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
const prefix = `${spaceId}-`;
|
|
58
|
+
return id.startsWith(prefix) && id.length > prefix.length ? id.substring(prefix.length) : null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Whether a prefix given in the library YAML is acceptable. It's the start of an id, so it
|
|
63
|
+
* may only use id characters, and no '-', so that the prefix of any id is unambiguous.
|
|
64
|
+
* 'core' is the endpoints' own core package, and 'x' is the code system factories
|
|
65
|
+
* (CodeSystem/x-...), so neither is available.
|
|
66
|
+
*
|
|
67
|
+
* @param {string} prefix
|
|
68
|
+
* @returns {string|null} why the prefix isn't acceptable, or null if it is
|
|
69
|
+
*/
|
|
70
|
+
function checkIdPrefix(prefix) {
|
|
71
|
+
if (!/^[A-Za-z0-9.]{1,32}$/.test(prefix)) {
|
|
72
|
+
return `an id prefix may only contain letters, digits and '.' (up to 32 characters)`;
|
|
73
|
+
}
|
|
74
|
+
if (prefix === CORE_PREFIX || prefix === 'x') {
|
|
75
|
+
return `the id prefix '${prefix}' is reserved`;
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Put a provider in an id space - the shared part of the providers' assignIds(spaceId).
|
|
82
|
+
* A provider is put in one id space, once: its resources are prefixed when it is, so
|
|
83
|
+
* moving it to another would leave them in the old one. Asking again for the same space
|
|
84
|
+
* does nothing.
|
|
85
|
+
*
|
|
86
|
+
* @param {Object} provider - a code system, value set or concept map provider
|
|
87
|
+
* @param {string} spaceId
|
|
88
|
+
* @returns {boolean} true if the provider has just been put in the space, and must now
|
|
89
|
+
* prefix its resources' ids
|
|
90
|
+
*/
|
|
91
|
+
function assignIdSpace(provider, spaceId) {
|
|
92
|
+
if (!spaceId) {
|
|
93
|
+
throw new Error(`${provider.constructor.name}: no id space given`);
|
|
94
|
+
}
|
|
95
|
+
if (provider.spaceId) {
|
|
96
|
+
if (provider.spaceId === spaceId) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
throw new Error(`${provider.constructor.name} is already in the id space '${provider.spaceId}', so it can't be put in '${spaceId}'`);
|
|
100
|
+
}
|
|
101
|
+
provider.spaceId = spaceId;
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const CORE_PREFIX = 'core';
|
|
106
|
+
|
|
107
|
+
module.exports = { applyIdPrefix, stripIdPrefix, checkIdPrefix, assignIdSpace, CORE_PREFIX };
|
package/tx/library.js
CHANGED
|
@@ -39,6 +39,7 @@ const { OCLValueSetProvider } = require('./ocl/vs-ocl');
|
|
|
39
39
|
const { OCLConceptMapProvider } = require('./ocl/cm-ocl');
|
|
40
40
|
const {UriServicesFactory} = require("./cs/cs-uri");
|
|
41
41
|
const {debugLog} = require("./operation-context");
|
|
42
|
+
const { checkIdPrefix } = require("./library/resource-ids");
|
|
42
43
|
|
|
43
44
|
/**
|
|
44
45
|
* This class holds all the loaded content ready for processing
|
|
@@ -72,6 +73,17 @@ class Library {
|
|
|
72
73
|
packageSources = [];
|
|
73
74
|
externalSources = [];
|
|
74
75
|
|
|
76
|
+
/**
|
|
77
|
+
* {Map<Object, string>} the id prefix the library YAML gives for the source a provider was
|
|
78
|
+
* loaded from (npm:hl7.terminology=tho), by provider
|
|
79
|
+
*/
|
|
80
|
+
#sourcePrefixes = new Map();
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* {Set<string>} every id prefix the library YAML gives - autonumbered prefixes skip them
|
|
84
|
+
*/
|
|
85
|
+
#givenPrefixes = new Set();
|
|
86
|
+
|
|
75
87
|
baseUrl = null;
|
|
76
88
|
cacheFolder = null;
|
|
77
89
|
startTime = Date.now();
|
|
@@ -171,6 +183,8 @@ class Library {
|
|
|
171
183
|
|
|
172
184
|
this.log.info('Fetching Data from '+this.baseUrl);
|
|
173
185
|
|
|
186
|
+
this.#checkSourcePrefixes(config.sources);
|
|
187
|
+
|
|
174
188
|
for (const source of config.sources) {
|
|
175
189
|
try {
|
|
176
190
|
await this.processSource(source, this.packageManager, "fetch");
|
|
@@ -223,15 +237,20 @@ class Library {
|
|
|
223
237
|
this.assignIds();
|
|
224
238
|
}
|
|
225
239
|
|
|
226
|
-
|
|
227
|
-
|
|
240
|
+
/**
|
|
241
|
+
* Parse a source entry from the library YAML: type[!]:details[=prefix]
|
|
242
|
+
*
|
|
243
|
+
* @param {string} source
|
|
244
|
+
* @returns {{type: string, details: string, isDefault: boolean, prefix: string|null}}
|
|
245
|
+
*/
|
|
246
|
+
#parseSource(source) {
|
|
228
247
|
const colonIndex = source.indexOf(':');
|
|
229
248
|
if (colonIndex === -1) {
|
|
230
249
|
throw new Error(`Invalid source format: ${source}`);
|
|
231
250
|
}
|
|
232
251
|
|
|
233
252
|
let type = source.substring(0, colonIndex);
|
|
234
|
-
|
|
253
|
+
let details = source.substring(colonIndex + 1);
|
|
235
254
|
|
|
236
255
|
// Handle special markers (like ! for default)
|
|
237
256
|
let isDefault = false;
|
|
@@ -240,6 +259,59 @@ class Library {
|
|
|
240
259
|
isDefault = true;
|
|
241
260
|
}
|
|
242
261
|
|
|
262
|
+
// the id prefix for the resources the source provides: everything after the last '='
|
|
263
|
+
let prefix = null;
|
|
264
|
+
const eqIndex = details.lastIndexOf('=');
|
|
265
|
+
if (eqIndex > -1) {
|
|
266
|
+
prefix = details.substring(eqIndex + 1);
|
|
267
|
+
details = details.substring(0, eqIndex);
|
|
268
|
+
const problem = checkIdPrefix(prefix);
|
|
269
|
+
if (problem) {
|
|
270
|
+
throw new Error(`Invalid source '${source}': ${problem}`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return { type, details, isDefault, prefix };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Check the id prefixes the library YAML gives before anything is loaded - each id space
|
|
278
|
+
* must belong to one source
|
|
279
|
+
*
|
|
280
|
+
* @param {string[]} sources
|
|
281
|
+
*/
|
|
282
|
+
#checkSourcePrefixes(sources) {
|
|
283
|
+
const seen = new Map();
|
|
284
|
+
for (const source of sources) {
|
|
285
|
+
const { prefix } = this.#parseSource(source);
|
|
286
|
+
if (prefix) {
|
|
287
|
+
if (seen.has(prefix)) {
|
|
288
|
+
throw new Error(`The id prefix '${prefix}' is given for both '${seen.get(prefix)}' and '${source}'`);
|
|
289
|
+
}
|
|
290
|
+
seen.set(prefix, source);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async processSource(source, packageManager, mode) {
|
|
296
|
+
const { type, details, isDefault, prefix } = this.#parseSource(source);
|
|
297
|
+
|
|
298
|
+
// the providers this source adds, so they can be given its id prefix
|
|
299
|
+
const csCount = this.codeSystemProviders.length;
|
|
300
|
+
const vsCount = this.valueSetProviders.length;
|
|
301
|
+
const cmCount = this.conceptMapProviders.length;
|
|
302
|
+
|
|
303
|
+
await this.#loadSource(type, details, isDefault, mode, packageManager);
|
|
304
|
+
|
|
305
|
+
if (prefix) {
|
|
306
|
+
this.#givenPrefixes.add(prefix);
|
|
307
|
+
for (const provider of [...this.codeSystemProviders.slice(csCount),
|
|
308
|
+
...this.valueSetProviders.slice(vsCount), ...this.conceptMapProviders.slice(cmCount)]) {
|
|
309
|
+
this.#sourcePrefixes.set(provider, prefix);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
async #loadSource(type, details, isDefault, mode, packageManager) {
|
|
243
315
|
// Switch statement for different source types
|
|
244
316
|
switch (type) {
|
|
245
317
|
case 'internal':
|
|
@@ -1000,6 +1072,7 @@ class Library {
|
|
|
1000
1072
|
// Now add the existing value set providers after the FHIR core packages
|
|
1001
1073
|
provider.valueSetProviders.push(...this.valueSetProviders);
|
|
1002
1074
|
provider.conceptMapProviders.push(...this.conceptMapProviders);
|
|
1075
|
+
provider.applyTHOPrecedence();
|
|
1003
1076
|
|
|
1004
1077
|
// bind UCUM common value set
|
|
1005
1078
|
let ucum = provider.codeSystemFactories.get("http://unitsofmeasure.org");
|
|
@@ -1030,30 +1103,29 @@ class Library {
|
|
|
1030
1103
|
}
|
|
1031
1104
|
|
|
1032
1105
|
/**
|
|
1033
|
-
*
|
|
1034
|
-
*
|
|
1035
|
-
* ids
|
|
1036
|
-
*
|
|
1106
|
+
* Every loaded resource is served in the id space of the provider that loaded it: its id is
|
|
1107
|
+
* the provider's spaceId, '-', and the id it had in its source (see
|
|
1108
|
+
* tx/library/resource-ids.js). The spaceId is the prefix the library YAML gives for the
|
|
1109
|
+
* provider's source, or, if it gives none, a number - the next one, counting providers of
|
|
1110
|
+
* the same kind, that the YAML doesn't use as a prefix. Numbers depend on the order of the
|
|
1111
|
+
* sources, so a source whose ids should stay the same when the YAML changes needs a prefix.
|
|
1112
|
+
*
|
|
1113
|
+
* (Code system factories don't have ids - they are not available directly.)
|
|
1037
1114
|
*/
|
|
1038
1115
|
assignIds() {
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
i = 0;
|
|
1052
|
-
for (const cmp of this.conceptMapProviders) {
|
|
1053
|
-
cmp.spaceId = String(++i);
|
|
1054
|
-
cmp.assignIds(ids);
|
|
1116
|
+
for (const providers of [this.codeSystemProviders, this.valueSetProviders, this.conceptMapProviders]) {
|
|
1117
|
+
let n = 0;
|
|
1118
|
+
for (const provider of providers) {
|
|
1119
|
+
let spaceId = this.#sourcePrefixes.get(provider);
|
|
1120
|
+
if (!spaceId) {
|
|
1121
|
+
do {
|
|
1122
|
+
n++;
|
|
1123
|
+
} while (this.#givenPrefixes.has(String(n)));
|
|
1124
|
+
spaceId = String(n);
|
|
1125
|
+
}
|
|
1126
|
+
provider.assignIds(spaceId);
|
|
1127
|
+
}
|
|
1055
1128
|
}
|
|
1056
|
-
|
|
1057
1129
|
}
|
|
1058
1130
|
|
|
1059
1131
|
async close() {
|
package/tx/ocl/cm-ocl.cjs
CHANGED
|
@@ -3,6 +3,7 @@ const { ConceptMap } = require('../library/conceptmap');
|
|
|
3
3
|
const { PAGE_SIZE } = require('./shared/constants');
|
|
4
4
|
const { createOclHttpClient } = require('./http/client');
|
|
5
5
|
const { fetchAllPages, extractItemsAndNext } = require('./http/pagination');
|
|
6
|
+
const { stripIdPrefix } = require('../library/resource-ids');
|
|
6
7
|
|
|
7
8
|
const DEFAULT_MAX_SEARCH_PAGES = 10;
|
|
8
9
|
|
|
@@ -24,7 +25,7 @@ class OCLConceptMapProvider extends AbstractConceptMapProvider {
|
|
|
24
25
|
this._canonicalBySourceUrl = new Map();
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
|
|
28
|
+
prefixIds() {
|
|
28
29
|
if (!this.spaceId) {
|
|
29
30
|
return;
|
|
30
31
|
}
|
|
@@ -33,13 +34,18 @@ class OCLConceptMapProvider extends AbstractConceptMapProvider {
|
|
|
33
34
|
this._idMap.clear();
|
|
34
35
|
|
|
35
36
|
for (const cm of unique) {
|
|
36
|
-
|
|
37
|
-
const nextId = `${this.spaceId}-${cm.id}`;
|
|
38
|
-
cm.id = nextId;
|
|
39
|
-
cm.jsonObj.id = nextId;
|
|
40
|
-
}
|
|
37
|
+
this.#prefixId(cm);
|
|
41
38
|
this._idMap.set(cm.id, cm);
|
|
42
|
-
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// concept maps are served in this provider's id space - including the ones that arrive
|
|
43
|
+
// after the spaceId is assigned
|
|
44
|
+
#prefixId(cm) {
|
|
45
|
+
if (this.spaceId && cm.id && !cm.id.startsWith(`${this.spaceId}-`)) {
|
|
46
|
+
const nextId = `${this.spaceId}-${cm.id}`;
|
|
47
|
+
cm.id = nextId;
|
|
48
|
+
cm.jsonObj.id = nextId;
|
|
43
49
|
}
|
|
44
50
|
}
|
|
45
51
|
|
|
@@ -56,7 +62,7 @@ class OCLConceptMapProvider extends AbstractConceptMapProvider {
|
|
|
56
62
|
|
|
57
63
|
const mappingId = this.#extractMappingId(url);
|
|
58
64
|
if (mappingId) {
|
|
59
|
-
return await this
|
|
65
|
+
return await this.#fetchMappingById(mappingId);
|
|
60
66
|
}
|
|
61
67
|
|
|
62
68
|
try {
|
|
@@ -81,14 +87,15 @@ class OCLConceptMapProvider extends AbstractConceptMapProvider {
|
|
|
81
87
|
if (this._idMap.has(id)) {
|
|
82
88
|
return this._idMap.get(id);
|
|
83
89
|
}
|
|
90
|
+
// only ids in this provider's space - the rest is the OCL mapping id
|
|
91
|
+
const rawId = stripIdPrefix(id, this.spaceId);
|
|
92
|
+
return rawId ? await this.#fetchMappingById(rawId) : null;
|
|
93
|
+
}
|
|
84
94
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
if (this._idMap.has(rawId)) {
|
|
91
|
-
return this._idMap.get(rawId);
|
|
95
|
+
async #fetchMappingById(rawId) {
|
|
96
|
+
const id = this.spaceId ? `${this.spaceId}-${rawId}` : rawId;
|
|
97
|
+
if (this._idMap.has(id)) {
|
|
98
|
+
return this._idMap.get(id);
|
|
92
99
|
}
|
|
93
100
|
|
|
94
101
|
try {
|
|
@@ -378,6 +385,7 @@ class OCLConceptMapProvider extends AbstractConceptMapProvider {
|
|
|
378
385
|
}
|
|
379
386
|
|
|
380
387
|
#indexConceptMap(cm) {
|
|
388
|
+
this.#prefixId(cm);
|
|
381
389
|
this.conceptMapMap.set(cm.url, cm);
|
|
382
390
|
if (cm.version) {
|
|
383
391
|
this.conceptMapMap.set(`${cm.url}|${cm.version}`, cm);
|
package/tx/ocl/cs-ocl.cjs
CHANGED
|
@@ -92,15 +92,12 @@ class OCLCodeSystemProvider extends AbstractCodeSystemProvider {
|
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
|
|
95
|
+
prefixIds() {
|
|
96
|
+
const codeSystems = [...new Set(this._idToCodeSystem.values())];
|
|
96
97
|
this._usedIds.clear();
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
cs.jsonObj.id = cs.id;
|
|
101
|
-
}
|
|
102
|
-
ids.add(`CodeSystem/${cs.id}`);
|
|
103
|
-
this._usedIds.add(cs.id);
|
|
98
|
+
this._idToCodeSystem.clear();
|
|
99
|
+
for (const cs of codeSystems) {
|
|
100
|
+
this.#trackCodeSystemId(cs);
|
|
104
101
|
this._idToCodeSystem.set(cs.id, cs);
|
|
105
102
|
}
|
|
106
103
|
}
|
|
@@ -358,9 +355,11 @@ class OCLCodeSystemProvider extends AbstractCodeSystemProvider {
|
|
|
358
355
|
return;
|
|
359
356
|
}
|
|
360
357
|
|
|
361
|
-
|
|
358
|
+
// code systems are served in this provider's id space
|
|
359
|
+
const prefix = this.spaceId ? `${this.spaceId}-` : '';
|
|
360
|
+
if (!cs.id || !cs.id.startsWith(prefix) || this._usedIds.has(cs.id)) {
|
|
362
361
|
const raw = cs.id || cs.name || cs.url || 'ocl-cs';
|
|
363
|
-
const base =
|
|
362
|
+
const base = raw.startsWith(prefix) ? String(raw) : `${prefix}${raw}`;
|
|
364
363
|
let candidate = base;
|
|
365
364
|
let index = 1;
|
|
366
365
|
while (this._usedIds.has(candidate)) {
|