fhirsmith 0.18.0 → 0.18.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 +31 -1
- package/library/daily-report.js +25 -15
- package/package.json +2 -2
- package/translations/Messages.properties +1 -0
- package/tx/closure/closure-store.js +14 -0
- package/tx/cs/cs-api.js +3 -1
- package/tx/cs/cs-cs.js +8 -2
- package/tx/cs/cs-icf.js +47 -34
- package/tx/cs/cs-loinc.js +249 -2
- package/tx/cs/cs-snomed.js +292 -25
- package/tx/importers/csv-records.js +35 -0
- package/tx/importers/import-loinc.module.js +177 -41
- package/tx/importers/import-sct.module.js +66 -1
- package/tx/importers/subset-loinc.module.js +25 -6
- package/tx/library/codesystem.js +4 -1
- package/tx/library/designations.js +23 -30
- package/tx/openapi-operations.js +2 -1
- package/tx/provider.js +10 -4
- package/tx/sct/structures.js +8 -2
- package/tx/tx.fhir.org.yml +2 -2
- package/tx/workers/closure.js +20 -0
- package/tx/workers/expand.js +2 -2
- package/tx/workers/validate.js +10 -0
- package/utilities/make-sct-loinc-subset.js +250 -0
package/tx/cs/cs-snomed.js
CHANGED
|
@@ -31,6 +31,18 @@ const Logger = require("../../library/logger");
|
|
|
31
31
|
// shared - it simply recomputes.
|
|
32
32
|
const SNOMED_FILTER_ANALYSIS = Symbol('snomedFilterAnalysis');
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Alternate identifier schemes (sct2_Identifier) that FHIRsmith knows how to present:
|
|
36
|
+
* the code system the identifiers are codes in, and the $lookup property they are
|
|
37
|
+
* returned as. An identifier in an unknown scheme is kept but not reported.
|
|
38
|
+
*/
|
|
39
|
+
const IDENTIFIER_SCHEMES = {
|
|
40
|
+
'30051010000102': { system: 'http://loinc.org', property: 'loinc' } // |LOINC code identifier| (LOINC Extension)
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
/** The $expand / ECL-free filter on alternate identifiers: mapped:<scheme> exists true|false */
|
|
44
|
+
const MAPPED_FILTER_PREFIX = 'mapped:';
|
|
45
|
+
|
|
34
46
|
// Description language index -> language code. The index is assigned at import
|
|
35
47
|
// time by SnomedImporter.mapLanguageCode(); this table is its inverse and must
|
|
36
48
|
// be kept in step with it.
|
|
@@ -192,6 +204,47 @@ class SnomedServices {
|
|
|
192
204
|
// means "could not decide", and leaves the value unchecked.
|
|
193
205
|
this.expressionServices.eclConstraintMatcher = (constraint, value) => this.mrcmRangeSatisfied(constraint, value);
|
|
194
206
|
this._mrcmRangeCache = new Map();
|
|
207
|
+
|
|
208
|
+
this.#loadIdentifiers(sharedData.identifiers || []);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Index the alternate identifiers (sct2_Identifier rows, active only, as
|
|
213
|
+
* [scheme, module, code, conceptId]) both ways: concept -> identifiers, and
|
|
214
|
+
* scheme|code -> concepts. Also which modules carry identifiers, since each such
|
|
215
|
+
* module defines an implicit map (?fhir_cm=<module>).
|
|
216
|
+
*/
|
|
217
|
+
#loadIdentifiers(rows) {
|
|
218
|
+
this.identifiersByConcept = new Map();
|
|
219
|
+
this.conceptsByIdentifier = new Map();
|
|
220
|
+
this.identifierModules = new Map(); // module -> Set of schemes
|
|
221
|
+
for (const [scheme, module, code, conceptId] of rows) {
|
|
222
|
+
const entry = { scheme, module, code };
|
|
223
|
+
if (!this.identifiersByConcept.has(conceptId)) {
|
|
224
|
+
this.identifiersByConcept.set(conceptId, []);
|
|
225
|
+
}
|
|
226
|
+
this.identifiersByConcept.get(conceptId).push(entry);
|
|
227
|
+
const key = scheme + '|' + code;
|
|
228
|
+
if (!this.conceptsByIdentifier.has(key)) {
|
|
229
|
+
this.conceptsByIdentifier.set(key, []);
|
|
230
|
+
}
|
|
231
|
+
this.conceptsByIdentifier.get(key).push(conceptId);
|
|
232
|
+
if (!this.identifierModules.has(module)) {
|
|
233
|
+
this.identifierModules.set(module, new Set());
|
|
234
|
+
}
|
|
235
|
+
this.identifierModules.get(module).add(scheme);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** The alternate identifiers of a concept (by id), optionally only those of one scheme */
|
|
240
|
+
getIdentifiers(conceptId, scheme = null) {
|
|
241
|
+
const list = this.identifiersByConcept.get(String(conceptId)) || [];
|
|
242
|
+
return scheme ? list.filter(i => i.scheme === scheme) : list;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** The concept ids that have the given code in the given scheme */
|
|
246
|
+
getConceptsForIdentifier(scheme, code) {
|
|
247
|
+
return this.conceptsByIdentifier.get(scheme + '|' + code) || [];
|
|
195
248
|
}
|
|
196
249
|
|
|
197
250
|
/**
|
|
@@ -2707,34 +2760,46 @@ class SnomedProvider extends BaseCSServices {
|
|
|
2707
2760
|
const ctxt = await this.#ensureContext(context);
|
|
2708
2761
|
if (ctxt) {
|
|
2709
2762
|
if (!(ctxt instanceof SnomedExpressionContext) || ctxt.expression?.concepts.length == 1) {
|
|
2710
|
-
|
|
2711
|
-
//
|
|
2712
|
-
//
|
|
2713
|
-
//
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2763
|
+
// Only what was asked for, if anything was (see _hasProp): effectiveTime, module,
|
|
2764
|
+
// parent, child and the defining relationships are returned by default but are not
|
|
2765
|
+
// important enough to return when the client named the properties it wants.
|
|
2766
|
+
// The mappings (alternate identifiers such as the LOINC code) are returned by
|
|
2767
|
+
// default too, and when asked for by name.
|
|
2768
|
+
if (this._hasProp(props, 'effectiveTime', true)) {
|
|
2769
|
+
const time = this.sct.concepts.getConcept(ctxt.getReference()).effectiveTime;
|
|
2770
|
+
// Pascal TDateTime epoch (1899-12-30) computed in UTC so the formatted
|
|
2771
|
+
// date is timezone-independent. new Date(1899,11,30) is LOCAL midnight,
|
|
2772
|
+
// which in zones ahead of UTC rolls the ISO date back by one day.
|
|
2773
|
+
const pascalEpochUTC = Date.UTC(1899, 11, 30);
|
|
2774
|
+
const date = new Date(pascalEpochUTC + time * 86400000);
|
|
2775
|
+
const dateStr = date.toISOString().slice(0, 10);
|
|
2776
|
+
this._addDateTimeProperty(params, 'property', 'effectiveTime', dateStr);
|
|
2777
|
+
}
|
|
2719
2778
|
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2779
|
+
if (this._hasProp(props, 'parent', true)) {
|
|
2780
|
+
const parents = this.sct.getConceptParents(ctxt.getReference());
|
|
2781
|
+
for (let parentRef of parents) {
|
|
2782
|
+
const code = this.sct.getConceptId(parentRef);
|
|
2783
|
+
const description = this.sct.getDisplayName(parentRef);
|
|
2784
|
+
this._addCodeProperty(params, 'property', 'parent', code, null, description);
|
|
2785
|
+
}
|
|
2725
2786
|
}
|
|
2726
2787
|
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2788
|
+
if (this._hasProp(props, 'child', true)) {
|
|
2789
|
+
const children = this.sct.getConceptChildren(ctxt.getReference());
|
|
2790
|
+
for (let childRef of children) {
|
|
2791
|
+
const code = this.sct.getConceptId(childRef);
|
|
2792
|
+
const description = this.sct.getDisplayName(childRef);
|
|
2793
|
+
this._addCodeProperty(params, 'property', 'child', code, null, description);
|
|
2794
|
+
}
|
|
2732
2795
|
}
|
|
2733
2796
|
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2797
|
+
if (this._hasProp(props, 'module', true)) {
|
|
2798
|
+
const moduleId = this.sct.concepts.getModuleId(ctxt.getReference());
|
|
2799
|
+
if (moduleId) {
|
|
2800
|
+
const code = this.sct.getConceptId(moduleId);
|
|
2801
|
+
this._addCodeProperty(params, 'property', 'module', code, null, this.sct.getDisplayName(moduleId));
|
|
2802
|
+
}
|
|
2738
2803
|
}
|
|
2739
2804
|
|
|
2740
2805
|
const relationships = this.sct.getConceptRelationships(ctxt.getReference());
|
|
@@ -2742,7 +2807,7 @@ class SnomedProvider extends BaseCSServices {
|
|
|
2742
2807
|
for (let relationshipRef of relationships) {
|
|
2743
2808
|
const relationship = this.sct.relationships.getRelationship(relationshipRef);
|
|
2744
2809
|
const relType = this.sct.getConceptId(relationship.relType);
|
|
2745
|
-
if (relType != '116680003') {
|
|
2810
|
+
if (relType != '116680003' && this._hasProp(props, relType, true)) {
|
|
2746
2811
|
const relTypeD = this.sct.getDisplayName(relationship.relType);
|
|
2747
2812
|
const code = this.sct.getConceptId(relationship.target);
|
|
2748
2813
|
const description = this.sct.getDisplayName(relationship.target);
|
|
@@ -2753,6 +2818,8 @@ class SnomedProvider extends BaseCSServices {
|
|
|
2753
2818
|
}
|
|
2754
2819
|
}
|
|
2755
2820
|
}
|
|
2821
|
+
|
|
2822
|
+
this.#addMappingProperties(ctxt, props, params);
|
|
2756
2823
|
}
|
|
2757
2824
|
if (ctxt instanceof SnomedExpressionContext) {
|
|
2758
2825
|
// ignore concepts for now, but list refinements and refinement groups
|
|
@@ -2768,6 +2835,27 @@ class SnomedProvider extends BaseCSServices {
|
|
|
2768
2835
|
}
|
|
2769
2836
|
}
|
|
2770
2837
|
|
|
2838
|
+
/**
|
|
2839
|
+
* The concept's alternate identifiers in the schemes we know (IDENTIFIER_SCHEMES), as
|
|
2840
|
+
* Coding-valued properties named for the scheme - e.g. loinc = http://loinc.org#2160-0.
|
|
2841
|
+
* Returned by default, and when asked for by name.
|
|
2842
|
+
*/
|
|
2843
|
+
#addMappingProperties(ctxt, props, params) {
|
|
2844
|
+
const conceptId = this.sct.getConceptId(ctxt.getReference());
|
|
2845
|
+
for (const ident of this.sct.getIdentifiers(conceptId)) {
|
|
2846
|
+
const scheme = IDENTIFIER_SCHEMES[ident.scheme];
|
|
2847
|
+
if (scheme && this._hasProp(props, scheme.property, true)) {
|
|
2848
|
+
params.push({
|
|
2849
|
+
name: 'property',
|
|
2850
|
+
part: [
|
|
2851
|
+
{ name: 'code', valueCode: scheme.property },
|
|
2852
|
+
{ name: 'value', valueCoding: { system: scheme.system, code: ident.code } }
|
|
2853
|
+
]
|
|
2854
|
+
});
|
|
2855
|
+
}
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
|
|
2771
2859
|
/**
|
|
2772
2860
|
* Report one refinement of a post-coordinated expression as a lookup property.
|
|
2773
2861
|
*
|
|
@@ -2823,6 +2911,11 @@ class SnomedProvider extends BaseCSServices {
|
|
|
2823
2911
|
return true;
|
|
2824
2912
|
}
|
|
2825
2913
|
|
|
2914
|
+
if (prop.startsWith(MAPPED_FILTER_PREFIX)) {
|
|
2915
|
+
return op === 'exists' && ['true', 'false'].includes(value) &&
|
|
2916
|
+
this.sct.stringToIdOrZero(prop.substring(MAPPED_FILTER_PREFIX.length)) !== 0n;
|
|
2917
|
+
}
|
|
2918
|
+
|
|
2826
2919
|
const cid = this.sct.stringToIdOrZero(prop);
|
|
2827
2920
|
if (cid != 0) {
|
|
2828
2921
|
const id = this.sct.stringToIdOrZero(value);
|
|
@@ -2979,6 +3072,27 @@ class SnomedProvider extends BaseCSServices {
|
|
|
2979
3072
|
return null;
|
|
2980
3073
|
}
|
|
2981
3074
|
|
|
3075
|
+
// mapped:<identifier scheme> exists true|false - concepts that do (or don't) have an
|
|
3076
|
+
// alternate identifier in that scheme (e.g. mapped:30051010000102: has a LOINC code).
|
|
3077
|
+
// A prefix, so as not to be confused with the attribute filters (<attribute> = <value>)
|
|
3078
|
+
if (prop.startsWith(MAPPED_FILTER_PREFIX)) {
|
|
3079
|
+
const scheme = prop.substring(MAPPED_FILTER_PREFIX.length);
|
|
3080
|
+
if (this.sct.stringToIdOrZero(scheme) === 0n) {
|
|
3081
|
+
throw new Error(`Invalid identifier scheme: ${scheme}`);
|
|
3082
|
+
}
|
|
3083
|
+
if (op !== 'exists') {
|
|
3084
|
+
throw new Error(`Unsupported filter operation: ${prop} ${op} ${value}`);
|
|
3085
|
+
}
|
|
3086
|
+
if (value !== 'true' && value !== 'false') {
|
|
3087
|
+
throw new Error(`Invalid filter value: ${value}`);
|
|
3088
|
+
}
|
|
3089
|
+
const filter = new SnomedFilterContext();
|
|
3090
|
+
filter.mappedScheme = scheme;
|
|
3091
|
+
filter.mappedExists = value === 'true';
|
|
3092
|
+
filterContext.filters.push(filter);
|
|
3093
|
+
return null;
|
|
3094
|
+
}
|
|
3095
|
+
|
|
2982
3096
|
const cid = this.sct.stringToIdOrZero(prop);
|
|
2983
3097
|
if (cid != 0) {
|
|
2984
3098
|
|
|
@@ -3082,7 +3196,9 @@ class SnomedProvider extends BaseCSServices {
|
|
|
3082
3196
|
}
|
|
3083
3197
|
let found = false;
|
|
3084
3198
|
|
|
3085
|
-
if (set.
|
|
3199
|
+
if (set.mappedScheme !== undefined) {
|
|
3200
|
+
found = this.#isMapped(reference, set.mappedScheme) === set.mappedExists;
|
|
3201
|
+
} else if (set.inactive !== undefined) {
|
|
3086
3202
|
let concept = this.sct.concepts.getConcept(reference);
|
|
3087
3203
|
let active = (concept.flags & 0x0F) === 0;
|
|
3088
3204
|
found = active !== set.inactive
|
|
@@ -3128,6 +3244,9 @@ class SnomedProvider extends BaseCSServices {
|
|
|
3128
3244
|
}
|
|
3129
3245
|
|
|
3130
3246
|
const reference = concept.getReference();
|
|
3247
|
+
if (set.mappedScheme !== undefined) {
|
|
3248
|
+
return this.#isMapped(reference, set.mappedScheme) === set.mappedExists;
|
|
3249
|
+
}
|
|
3131
3250
|
if (set.inactive !== undefined) {
|
|
3132
3251
|
return this.sct.isActive(reference) !== set.inactive;
|
|
3133
3252
|
}
|
|
@@ -3187,10 +3306,39 @@ class SnomedProvider extends BaseCSServices {
|
|
|
3187
3306
|
return set._matchSet.has(reference);
|
|
3188
3307
|
}
|
|
3189
3308
|
|
|
3309
|
+
/** Whether the concept (by reference) has an alternate identifier in the scheme */
|
|
3310
|
+
#isMapped(reference, scheme) {
|
|
3311
|
+
return this.sct.getIdentifiers(this.sct.getConceptId(reference), scheme).length > 0;
|
|
3312
|
+
}
|
|
3313
|
+
|
|
3190
3314
|
#ensurePopulated(set) {
|
|
3191
3315
|
if (set.populationDone) {
|
|
3192
3316
|
return;
|
|
3193
3317
|
}
|
|
3318
|
+
if (set.mappedScheme !== undefined) {
|
|
3319
|
+
if (set.mappedExists) {
|
|
3320
|
+
// straight from the identifier index, in concept order
|
|
3321
|
+
const refs = new Set();
|
|
3322
|
+
for (const [conceptId, idents] of this.sct.identifiersByConcept) {
|
|
3323
|
+
if (idents.some(i => i.scheme === set.mappedScheme)) {
|
|
3324
|
+
const r = this.sct.concepts.findConcept(BigInt(conceptId));
|
|
3325
|
+
if (r.found) {
|
|
3326
|
+
refs.add(r.index);
|
|
3327
|
+
}
|
|
3328
|
+
}
|
|
3329
|
+
}
|
|
3330
|
+
set.descendants = [...refs].sort((a, b) => a - b);
|
|
3331
|
+
} else {
|
|
3332
|
+
for (let i = 0; i < this.sct.concepts.count(); i++) {
|
|
3333
|
+
const concept = this.sct.concepts.getConceptByCount(i);
|
|
3334
|
+
if (!this.#isMapped(concept.index, set.mappedScheme)) {
|
|
3335
|
+
set.descendants.push(concept.index);
|
|
3336
|
+
}
|
|
3337
|
+
}
|
|
3338
|
+
}
|
|
3339
|
+
set.populationDone = true;
|
|
3340
|
+
return;
|
|
3341
|
+
}
|
|
3194
3342
|
if (set.inactive !== undefined && set.descendants.length === 0) {
|
|
3195
3343
|
for (let i = 0; i < this.sct.concepts.count(); i++) {
|
|
3196
3344
|
let concept = this.sct.concepts.getConceptByCount(i);
|
|
@@ -3342,6 +3490,9 @@ class SnomedProvider extends BaseCSServices {
|
|
|
3342
3490
|
}
|
|
3343
3491
|
|
|
3344
3492
|
async getTranslations(map, coding, target, reverse) {
|
|
3493
|
+
if (map && map.jsonObj.identifierMap) {
|
|
3494
|
+
return this.translateByIdentifier(map, coding, target);
|
|
3495
|
+
}
|
|
3345
3496
|
if (!map || reverse) {
|
|
3346
3497
|
return [];
|
|
3347
3498
|
}
|
|
@@ -3405,6 +3556,54 @@ class SnomedProvider extends BaseCSServices {
|
|
|
3405
3556
|
return result;
|
|
3406
3557
|
}
|
|
3407
3558
|
|
|
3559
|
+
/**
|
|
3560
|
+
* Translate using a module's alternate identifiers (the implicit map ?fhir_cm=<module>,
|
|
3561
|
+
* e.g. the LOINC Extension's LOINC codes). The identifiers say the concept and the code
|
|
3562
|
+
* are the same thing, so the map works the same in both directions: a SNOMED CT concept
|
|
3563
|
+
* translates to its codes, and a code in the identifier's system to its concepts. Either
|
|
3564
|
+
* way the relationship is equivalent.
|
|
3565
|
+
*/
|
|
3566
|
+
async translateByIdentifier(map, coding, target) {
|
|
3567
|
+
const { schemes } = map.jsonObj.identifierMap;
|
|
3568
|
+
const targetSystem = target ? target.split('|')[0] : null;
|
|
3569
|
+
const result = [];
|
|
3570
|
+
if (coding.system === this.system()) {
|
|
3571
|
+
const ctxt = await this.locate(coding.code);
|
|
3572
|
+
if (!ctxt.context) {
|
|
3573
|
+
return [];
|
|
3574
|
+
}
|
|
3575
|
+
const conceptId = this.sct.getConceptId(ctxt.context.getReference());
|
|
3576
|
+
for (const ident of this.sct.getIdentifiers(conceptId)) {
|
|
3577
|
+
const scheme = IDENTIFIER_SCHEMES[ident.scheme];
|
|
3578
|
+
if (scheme && schemes.includes(ident.scheme) && (!targetSystem || targetSystem === scheme.system)) {
|
|
3579
|
+
result.push({
|
|
3580
|
+
map: map.vurl,
|
|
3581
|
+
system: scheme.system,
|
|
3582
|
+
code: ident.code,
|
|
3583
|
+
relationship: 'equivalent'
|
|
3584
|
+
});
|
|
3585
|
+
}
|
|
3586
|
+
}
|
|
3587
|
+
} else if (!targetSystem || targetSystem === this.system()) {
|
|
3588
|
+
for (const schemeId of schemes) {
|
|
3589
|
+
const scheme = IDENTIFIER_SCHEMES[schemeId];
|
|
3590
|
+
if (scheme && scheme.system === coding.system) {
|
|
3591
|
+
for (const conceptId of this.sct.getConceptsForIdentifier(schemeId, coding.code)) {
|
|
3592
|
+
result.push({
|
|
3593
|
+
map: map.vurl,
|
|
3594
|
+
system: this.system(),
|
|
3595
|
+
version: this.version(),
|
|
3596
|
+
code: conceptId,
|
|
3597
|
+
display: await this.display(conceptId),
|
|
3598
|
+
relationship: 'equivalent'
|
|
3599
|
+
});
|
|
3600
|
+
}
|
|
3601
|
+
}
|
|
3602
|
+
}
|
|
3603
|
+
}
|
|
3604
|
+
return result;
|
|
3605
|
+
}
|
|
3606
|
+
|
|
3408
3607
|
/**
|
|
3409
3608
|
* Translate a concept of this edition to the edition it depends on (typically a national
|
|
3410
3609
|
* extension to the International Edition). There is no map: the answer comes from the
|
|
@@ -3789,6 +3988,9 @@ class SnomedServicesFactory extends CodeSystemFactoryProvider {
|
|
|
3789
3988
|
* edition URI plus ?fhir_cm=edition/<target edition module>
|
|
3790
3989
|
*/
|
|
3791
3990
|
async findImplicitConceptMaps(conceptMaps, source, sourceVersion, dest) {
|
|
3991
|
+
if (dest) {
|
|
3992
|
+
this.#findIdentifierMaps(conceptMaps, source, sourceVersion, dest);
|
|
3993
|
+
}
|
|
3792
3994
|
if (source !== this.system() || !sourceVersion || !dest || !this.isMyEdition(sourceVersion)) {
|
|
3793
3995
|
return;
|
|
3794
3996
|
}
|
|
@@ -3823,6 +4025,61 @@ class SnomedServicesFactory extends CodeSystemFactoryProvider {
|
|
|
3823
4025
|
});
|
|
3824
4026
|
}
|
|
3825
4027
|
|
|
4028
|
+
/**
|
|
4029
|
+
* Maps implied by alternate identifiers, found from the systems rather than nominated:
|
|
4030
|
+
* from this edition (source system|version naming it) to an identifier system (LOINC,
|
|
4031
|
+
* say), or from an identifier system to this edition (the target given as
|
|
4032
|
+
* system|edition URI, with or without the version)
|
|
4033
|
+
*/
|
|
4034
|
+
#findIdentifierMaps(conceptMaps, source, sourceVersion, dest) {
|
|
4035
|
+
const bar = dest.indexOf('|');
|
|
4036
|
+
const destSystem = bar === -1 ? dest : dest.substring(0, bar);
|
|
4037
|
+
const destVersion = bar === -1 ? null : dest.substring(bar + 1);
|
|
4038
|
+
let identSystem;
|
|
4039
|
+
if (source === this.system() && sourceVersion && this.isMyEdition(sourceVersion)) {
|
|
4040
|
+
identSystem = destSystem;
|
|
4041
|
+
} else if (destSystem === this.system() && destVersion && this.isMyEdition(destVersion)) {
|
|
4042
|
+
identSystem = source;
|
|
4043
|
+
} else {
|
|
4044
|
+
return;
|
|
4045
|
+
}
|
|
4046
|
+
for (const [moduleId, schemes] of this.snomedServices.identifierModules) {
|
|
4047
|
+
if ([...schemes].some(s => IDENTIFIER_SCHEMES[s] && IDENTIFIER_SCHEMES[s].system === identSystem)) {
|
|
4048
|
+
const cm = this.identifierMap(moduleId);
|
|
4049
|
+
if (cm && !conceptMaps.some(m => m.url === cm.url)) {
|
|
4050
|
+
conceptMaps.push(cm);
|
|
4051
|
+
}
|
|
4052
|
+
}
|
|
4053
|
+
}
|
|
4054
|
+
}
|
|
4055
|
+
|
|
4056
|
+
/**
|
|
4057
|
+
* The implicit map defined by a module's alternate identifiers, if the module has any
|
|
4058
|
+
* in a scheme we know. Its url follows the edition maps: <source edition>/version/<date>?fhir_cm=<module>
|
|
4059
|
+
*/
|
|
4060
|
+
identifierMap(moduleId) {
|
|
4061
|
+
const schemes = [...(this.snomedServices.identifierModules.get(moduleId) || [])].filter(s => IDENTIFIER_SCHEMES[s]);
|
|
4062
|
+
if (schemes.length === 0) {
|
|
4063
|
+
return null;
|
|
4064
|
+
}
|
|
4065
|
+
const systems = schemes.map(s => IDENTIFIER_SCHEMES[s].system);
|
|
4066
|
+
const moduleName = this.snomedServices.conceptExists(moduleId)
|
|
4067
|
+
? this.snomedServices.getDisplayName(this.snomedServices.concepts.findConcept(BigInt(moduleId)).index) : moduleId;
|
|
4068
|
+
return new ConceptMap({
|
|
4069
|
+
resourceType: 'ConceptMap',
|
|
4070
|
+
internalSource: this,
|
|
4071
|
+
identifierMap: { module: moduleId, schemes: schemes },
|
|
4072
|
+
url: `${this.version()}?fhir_cm=${moduleId}`,
|
|
4073
|
+
name: `SNOMED CT ${moduleName} Concept Map`,
|
|
4074
|
+
description: `The concept map implied by the alternate identifiers (${systems.join(', ')}) of the module ${moduleId} |${moduleName}| in ${this.version()}`,
|
|
4075
|
+
status: 'active',
|
|
4076
|
+
group: [
|
|
4077
|
+
...systems.map(s => ({ source: this.system(), target: s })),
|
|
4078
|
+
...systems.map(s => ({ source: s, target: this.system() }))
|
|
4079
|
+
]
|
|
4080
|
+
});
|
|
4081
|
+
}
|
|
4082
|
+
|
|
3826
4083
|
/** Whether a source version names this edition: exactly, or the edition without a version */
|
|
3827
4084
|
isMyEdition(version) {
|
|
3828
4085
|
const mine = this.version();
|
|
@@ -3846,6 +4103,16 @@ class SnomedServicesFactory extends CodeSystemFactoryProvider {
|
|
|
3846
4103
|
const dep = this.snomedServices.editionDependency(`http://snomed.info/sct/${url.substring(q + '?fhir_cm=edition/'.length)}`);
|
|
3847
4104
|
return dep ? this.editionTranslationMap(dep) : null;
|
|
3848
4105
|
}
|
|
4106
|
+
// <edition[/version/date]>?fhir_cm=<module>: the map implied by a module's alternate
|
|
4107
|
+
// identifiers (e.g. 11010000107, the LOINC Extension's LOINC codes)
|
|
4108
|
+
const qm = url.indexOf('?fhir_cm=');
|
|
4109
|
+
if (qm !== -1 && url.substring(0, qm) !== this.system()) {
|
|
4110
|
+
if (!this.isMyEdition(url.substring(0, qm))) {
|
|
4111
|
+
return null;
|
|
4112
|
+
}
|
|
4113
|
+
await this.#ensureLoaded();
|
|
4114
|
+
return this.identifierMap(url.substring(qm + '?fhir_cm='.length));
|
|
4115
|
+
}
|
|
3849
4116
|
if (!url.startsWith(this.system()+"?fhir_cm=")) {
|
|
3850
4117
|
return null;
|
|
3851
4118
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const readline = require('readline');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Yields the records of a CSV file one at a time, as text.
|
|
6
|
+
*
|
|
7
|
+
* A quoted field can contain a line break (LOINC 2.83's ComponentHierarchyBySystem.csv has one
|
|
8
|
+
* in a CODE_TEXT), so a record that ends inside quotes is joined to the next line until the
|
|
9
|
+
* quotes balance. An escaped quote ("") counts twice, so it doesn't upset the count.
|
|
10
|
+
*/
|
|
11
|
+
async function* csvRecords(filePath) {
|
|
12
|
+
const rl = readline.createInterface({
|
|
13
|
+
input: fs.createReadStream(filePath),
|
|
14
|
+
crlfDelay: Infinity
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
let pending = null;
|
|
18
|
+
let quotes = 0;
|
|
19
|
+
for await (const line of rl) {
|
|
20
|
+
pending = pending === null ? line : pending + '\n' + line;
|
|
21
|
+
for (let i = 0; i < line.length; i++) {
|
|
22
|
+
if (line.charCodeAt(i) === 34) quotes++;
|
|
23
|
+
}
|
|
24
|
+
if (quotes % 2 === 0) {
|
|
25
|
+
yield pending;
|
|
26
|
+
pending = null;
|
|
27
|
+
quotes = 0;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (pending !== null) {
|
|
31
|
+
yield pending;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = { csvRecords };
|