fhirsmith 0.17.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 +113 -0
- package/README.md +80 -13
- package/config-template.json +106 -1
- package/ips/ai-client.js +263 -0
- package/ips/composer.js +805 -0
- package/ips/editor-schema.js +289 -0
- package/ips/editor.js +290 -0
- package/ips/fhirpath-ips.js +397 -0
- package/ips/ips-codeable-concepts.md +1742 -0
- package/ips/ips-template.html +125 -0
- package/ips/ips-valuesets.js +159 -0
- package/ips/ips.js +1736 -0
- package/ips/membership.js +179 -0
- package/ips/pipeline.js +1012 -0
- package/ips/presenter.js +974 -0
- package/ips/profile-editor.js +578 -0
- package/ips/profiles.js +506 -0
- package/ips/readme.md +460 -0
- package/ips/render.js +1482 -0
- package/ips/search-params.js +83 -0
- package/ips/search.js +667 -0
- package/ips/share.js +178 -0
- package/ips/store.js +650 -0
- package/ips/terminology.js +395 -0
- package/ips/tx-client.js +202 -0
- package/ips/valuesets/CodeSystem-absent-unknown-uv-ips.json +206 -0
- package/ips/valuesets/ValueSet-absent-or-unknown-immunizations-uv-ips.json +59 -0
- package/ips/valuesets/ValueSet-ips-allergy-code.json +59 -0
- package/ips/valuesets/ValueSet-ips-allergy-manifestation.json +356 -0
- package/ips/valuesets/ValueSet-ips-allergy-substance.json +37 -0
- package/ips/valuesets/ValueSet-ips-body-site.json +26 -0
- package/ips/valuesets/ValueSet-ips-clinical-finding-or-none.json +35 -0
- package/ips/valuesets/ValueSet-ips-clinical-finding.json +26 -0
- package/ips/valuesets/ValueSet-ips-condition-code.json +72 -0
- package/ips/valuesets/ValueSet-ips-condition-severity.json +30 -0
- package/ips/valuesets/ValueSet-ips-condition-stage.json +26 -0
- package/ips/valuesets/ValueSet-ips-exposure-route.json +26 -0
- package/ips/valuesets/ValueSet-ips-imaging-procedure.json +25 -0
- package/ips/valuesets/ValueSet-ips-immunization-status-reason.json +43 -0
- package/ips/valuesets/ValueSet-ips-international-codes.json +25 -0
- package/ips/valuesets/ValueSet-ips-medication-ingredient.json +26 -0
- package/ips/valuesets/ValueSet-ips-medication-status-reason.json +37 -0
- package/ips/valuesets/ValueSet-ips-medication.json +50 -0
- package/ips/valuesets/ValueSet-ips-observation-code.json +18 -0
- package/ips/valuesets/ValueSet-ips-observation-interpretation.json +18 -0
- package/ips/valuesets/ValueSet-ips-procedure-followup.json +51 -0
- package/ips/valuesets/ValueSet-ips-procedure-outcome.json +30 -0
- package/ips/valuesets/ValueSet-ips-procedure-reason.json +37 -0
- package/ips/valuesets/ValueSet-ips-procedure.json +116 -0
- package/ips/valuesets/ValueSet-ips-route.json +46 -0
- package/ips/valuesets/ValueSet-ips-target-disease.json +171 -0
- package/ips/valuesets/ValueSet-ips-vaccine.json +31 -0
- package/ips/valuesets/elements.json +44 -0
- package/library/daily-report.js +432 -0
- package/library/fhir-search-values.js +138 -0
- package/library/html-server.js +32 -23
- package/library/mailer.js +77 -0
- package/library/narrative-check.js +21 -2
- package/library/npm-audit.js +25 -0
- package/library/package-manager.js +4 -2
- package/library/sqlite-session-store.js +29 -1
- package/package.json +8 -3
- package/packages/packages.js +34 -3
- package/publisher/publisher.js +29 -0
- package/registry/auth-store.js +366 -0
- package/registry/auth.js +872 -0
- package/registry/crawler.js +41 -1
- package/registry/documentation/api.md +162 -0
- package/registry/documentation/configuration.md +93 -0
- package/registry/documentation/documents.md +74 -0
- package/registry/documentation/internals.md +151 -0
- package/registry/documentation/readme.md +100 -0
- package/registry/documentation/security.md +199 -0
- package/registry/documents.js +309 -0
- package/registry/openapi.yaml +30 -0
- package/registry/registry-template.html +1 -1
- package/registry/registry.js +230 -44
- package/registry/uptime.js +154 -0
- package/server.js +105 -44
- package/shl/shl.js +64 -9
- package/stats-db.js +93 -0
- package/stats.js +158 -1
- package/testing/search.js +2 -108
- package/testing/store.js +5 -0
- package/testing/testing.js +20 -0
- package/token/token-template.html +5 -4
- package/token/token.js +17 -10
- package/translations/Messages.properties +10 -0
- package/tx/README.md +52 -0
- package/tx/closure/closure-store.js +14 -0
- package/tx/cs/cs-api.js +92 -14
- package/tx/cs/cs-cs.js +64 -3
- package/tx/cs/cs-icd11.js +366 -112
- package/tx/cs/cs-icf.js +431 -0
- package/tx/cs/cs-loinc.js +296 -27
- package/tx/cs/cs-omop.js +10 -3
- package/tx/cs/cs-snomed.js +1210 -80
- package/tx/cs/cs-ucum.js +6 -0
- package/tx/cs/icd11-expressions.js +191 -78
- package/tx/importers/csv-records.js +35 -0
- package/tx/importers/export-icd11-codesystem.js +58 -12
- package/tx/importers/icd11-schema.md +9 -5
- 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/library.js +98 -4
- package/tx/openapi-operations.js +6 -1
- package/tx/params.js +8 -1
- package/tx/provider.js +24 -6
- package/tx/sct/documentation.md +251 -0
- package/tx/sct/ecl.js +758 -46
- package/tx/sct/expressions.js +113 -52
- package/tx/sct/structures.js +8 -2
- package/tx/syndication/syndication-feed.js +218 -0
- package/tx/syndication/syndication-store.js +415 -0
- package/tx/tx-html.js +15 -1
- package/tx/tx.fhir.org.yml +3 -4
- package/tx/tx.js +144 -2
- package/tx/vs/vs-vsac.js +25 -6
- package/tx/workers/closure.js +20 -0
- package/tx/workers/expand.js +35 -8
- package/tx/workers/lookup.js +61 -6
- package/tx/workers/subsumes.js +1 -1
- package/tx/workers/translate.js +166 -15
- package/tx/workers/validate.js +138 -6
- package/utilities/make-sct-loinc-subset.js +250 -0
- package/utilities/make-sct-test-extension.js +264 -0
- package/xig/xig.js +32 -0
- package/registry/readme.md +0 -124
package/tx/cs/cs-icd11.js
CHANGED
|
@@ -18,12 +18,21 @@ const { CodeSystem } = require('../library/codesystem');
|
|
|
18
18
|
const { CodeSystemFactoryProvider, FilterExecutionContext } = require('./cs-api');
|
|
19
19
|
const { BaseCSServices } = require('./cs-base');
|
|
20
20
|
const { validateArrayParameter } = require('../../library/utilities');
|
|
21
|
-
const { ICD11Expression, parseExpression } = require('./icd11-expressions');
|
|
21
|
+
const { ICD11Expression, parseExpression, axisLabel } = require('./icd11-expressions');
|
|
22
22
|
const { Issue } = require('../library/operation-outcome');
|
|
23
|
+
const { DesignationUse } = require('../library/designations');
|
|
23
24
|
const assert = require('assert');
|
|
24
25
|
|
|
25
26
|
// WHO's own vocabulary for the kinds of text an entity carries, used as designation.use.
|
|
26
27
|
const SCHEMA_NS = 'http://id.who.int/icd/schema';
|
|
28
|
+
const FOUNDATION_URL = 'http://id.who.int/icd/entity';
|
|
29
|
+
|
|
30
|
+
// The extension a Coding may carry to save the reader a lookup: the short code (or the
|
|
31
|
+
// short-code form of an expression) for the entity id in Coding.code. Provisional url.
|
|
32
|
+
const CLASSIFICATION_CODE_EXT = 'http://id.who.int/icd/schema/extension/classificationCode';
|
|
33
|
+
|
|
34
|
+
// The code system of allowMultipleValues: AllowAlways | NotAllowed | AllowedExceptFromSameBlock
|
|
35
|
+
const ALLOW_MULTIPLE_CS = `${SCHEMA_NS}/allowMultipleValues`;
|
|
27
36
|
|
|
28
37
|
// designation.kind values that are not designations: `definition` is
|
|
29
38
|
// CodeSystem.concept.definition, and is returned by definition() instead.
|
|
@@ -34,7 +43,12 @@ const NOT_A_DESIGNATION = new Set(['definition']);
|
|
|
34
43
|
* enough of the concept row that the cheap accessors never go back to the database.
|
|
35
44
|
*/
|
|
36
45
|
class ICD11Concept {
|
|
37
|
-
|
|
46
|
+
/**
|
|
47
|
+
* @param row the concept row
|
|
48
|
+
* @param bareIds true for the linearizations (MMS, ICF), whose code is the bare entity id;
|
|
49
|
+
* false for the Foundation, whose code is still the entity uri
|
|
50
|
+
*/
|
|
51
|
+
constructor(row, bareIds) {
|
|
38
52
|
this.id = row.id; // internal rowid, used for joins
|
|
39
53
|
this.entityId = row.entity_id; // 257068234, or 1363559646/other for a residual
|
|
40
54
|
this.uri = row.uri; // unversioned canonical uri
|
|
@@ -49,17 +63,18 @@ class ICD11Concept {
|
|
|
49
63
|
this.browserUrl = row.browser_url;
|
|
50
64
|
this.lft = row.lft; // nested set; null for the Foundation (a DAG)
|
|
51
65
|
this.rgt = row.rgt;
|
|
66
|
+
this.bareIds = !!bareIds;
|
|
52
67
|
}
|
|
53
68
|
|
|
54
69
|
/**
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
70
|
+
* The concept's code. In MMS and ICF it is the bare entity id (257068234, or
|
|
71
|
+
* 1363559646/other for a residual): the full uri is the system url + '/' + the id, so
|
|
72
|
+
* the prefix carries nothing (the October 2026 WHO meeting). The short code (this.code)
|
|
73
|
+
* is its classificationCode, reported beside it. The Foundation was not part of that
|
|
74
|
+
* decision, and keeps the entity uri.
|
|
60
75
|
*/
|
|
61
76
|
get canonicalCode() {
|
|
62
|
-
return this.
|
|
77
|
+
return this.bareIds ? this.entityId : this.uri;
|
|
63
78
|
}
|
|
64
79
|
}
|
|
65
80
|
|
|
@@ -105,19 +120,41 @@ class ICD11Services extends BaseCSServices {
|
|
|
105
120
|
});
|
|
106
121
|
}
|
|
107
122
|
|
|
123
|
+
/** MMS and ICF codes are bare entity ids; the Foundation's are still entity uris. */
|
|
124
|
+
get #bareIds() { return this.shared.systemCode !== 'foundation'; }
|
|
125
|
+
|
|
126
|
+
#concept(row) {
|
|
127
|
+
const c = new ICD11Concept(row, this.#bareIds);
|
|
128
|
+
// "selectable" is a linearization's notion - a category with a short code. The importer
|
|
129
|
+
// records 0 for every Foundation entity, which has neither; but nothing in the Foundation is
|
|
130
|
+
// a grouper in that sense, so none of it is reported as not selectable
|
|
131
|
+
if (!this.#bareIds) c.selectable = true;
|
|
132
|
+
return c;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** The code of a concept row, for references to it (parent, child, stem, values). */
|
|
136
|
+
#ref(row) { return this.#bareIds ? row.entity_id : row.uri; }
|
|
137
|
+
|
|
108
138
|
// -- metadata -------------------------------------------------------------------------
|
|
109
139
|
|
|
110
140
|
system() { return this.shared.url; }
|
|
111
141
|
version() { return this.shared.version; }
|
|
112
142
|
|
|
113
143
|
/**
|
|
114
|
-
*
|
|
115
|
-
*
|
|
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.
|
|
144
|
+
* CodeSystem.name, as export-icd11-codesystem.js writes it: ICD11MMS, ICD11ICF,
|
|
145
|
+
* ICD11FOUNDATION.
|
|
119
146
|
*/
|
|
120
|
-
name(
|
|
147
|
+
name() { return `ICD11${this.shared.systemCode.toUpperCase()}`; }
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* $lookup's display name: the linearization title in the request's language where WHO
|
|
151
|
+
* translated it, else the primary title. $lookup passes its working languages
|
|
152
|
+
* (displayLanguage, else Accept-Language), since displayLanguage never reaches
|
|
153
|
+
* opContext.langs.
|
|
154
|
+
*/
|
|
155
|
+
lookupName(languages) {
|
|
156
|
+
return this.shared.titles?.[this.#displayLang(languages)] || this.shared.title || this.name();
|
|
157
|
+
}
|
|
121
158
|
description() { return this.shared.title; }
|
|
122
159
|
totalCount() { return this.shared.count; }
|
|
123
160
|
defLang() { return 'en'; }
|
|
@@ -164,8 +201,8 @@ class ICD11Services extends BaseCSServices {
|
|
|
164
201
|
// -- locating -------------------------------------------------------------------------
|
|
165
202
|
|
|
166
203
|
/**
|
|
167
|
-
*
|
|
168
|
-
* accepts. Returns { context, message }; a plain miss carries no message, which the
|
|
204
|
+
* MMS and ICF accept the bare entity id (the code) and the short classification code;
|
|
205
|
+
* the Foundation accepts its entity uri and the short code. Returns { context, message }; a plain miss carries no message, which the
|
|
169
206
|
* validation worker treats as "not there" rather than "malformed".
|
|
170
207
|
*/
|
|
171
208
|
async locate(code) {
|
|
@@ -186,12 +223,18 @@ class ICD11Services extends BaseCSServices {
|
|
|
186
223
|
return { context: null, message: undefined };
|
|
187
224
|
}
|
|
188
225
|
|
|
189
|
-
/**
|
|
226
|
+
/**
|
|
227
|
+
* A plain code as written - the short code, or the entity id (MMS, ICF) or uri
|
|
228
|
+
* (Foundation); null if it is none of them. An id is looked up as system + '/' + id,
|
|
229
|
+
* which is what its uri is, so the uri index serves both. The full uri is not accepted
|
|
230
|
+
* in MMS or ICF: it is not a code there.
|
|
231
|
+
*/
|
|
190
232
|
async #resolve(code) {
|
|
233
|
+
const uri = this.#bareIds ? `${this.shared.url}/${code}` : code;
|
|
191
234
|
const row = await this.#get(
|
|
192
235
|
`SELECT * FROM concept WHERE system = ? AND (code = ? OR uri = ?)`,
|
|
193
|
-
[this.shared.systemId, code,
|
|
194
|
-
return row ?
|
|
236
|
+
[this.shared.systemId, code, uri]);
|
|
237
|
+
return row ? this.#concept(row) : null;
|
|
195
238
|
}
|
|
196
239
|
|
|
197
240
|
/** What the expression parser needs: the database, and the request's translations. */
|
|
@@ -260,6 +303,74 @@ class ICD11Services extends BaseCSServices {
|
|
|
260
303
|
return ctxt === null ? null : ctxt.canonicalCode;
|
|
261
304
|
}
|
|
262
305
|
|
|
306
|
+
/** The short-code form (1A00, or 1A00&XN8P1 for an expression), where there is one. */
|
|
307
|
+
async classificationCode(context) {
|
|
308
|
+
const ctxt = await this.#ensureContext(context);
|
|
309
|
+
if (ctxt === null) return null;
|
|
310
|
+
return (ctxt instanceof ICD11Expression ? ctxt.classificationCode : ctxt.code) || null;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** Value sets may enumerate short codes; the expansion carries the code, the uri. */
|
|
314
|
+
async expansionCode(context, requested) {
|
|
315
|
+
return (await this.code(context)) || requested;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
mandatoryExpansionProperties() { return ['classificationCode']; }
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* $validate-code reports the classificationCode only for a code written with entity ids - in
|
|
322
|
+
* any order, since the classificationCode is the short-code form of the normal form, beside
|
|
323
|
+
* normalized-code. A code written with short codes (wholly or in part) gets none: it already
|
|
324
|
+
* is, or contains, the classification code.
|
|
325
|
+
*/
|
|
326
|
+
async reportedClassificationCode(context, code) {
|
|
327
|
+
const ctxt = await this.#ensureContext(context);
|
|
328
|
+
if (ctxt === null) return null;
|
|
329
|
+
const idForm = ctxt instanceof ICD11Expression
|
|
330
|
+
? ctxt.tokens.every(t => t.text === t.concept.canonicalCode)
|
|
331
|
+
: code === ctxt.canonicalCode;
|
|
332
|
+
return idForm ? this.classificationCode(ctxt) : null;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** A postcoordinated expression's parse records what validation should say about it. */
|
|
336
|
+
async codeIssues(context) {
|
|
337
|
+
const ctxt = await this.#ensureContext(context);
|
|
338
|
+
return ctxt instanceof ICD11Expression ? ctxt.issues : [];
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* The classificationCode extension on a Coding, when present, has to be true: it is there
|
|
343
|
+
* so software can show the short code without a lookup, and a wrong one would be shown.
|
|
344
|
+
* It is defined for an MMS or ICF coding whose code is the entity id (or the id form of an
|
|
345
|
+
* expression) - a short code needs no other short code beside it - and its value has to be
|
|
346
|
+
* that code's classificationCode exactly.
|
|
347
|
+
*/
|
|
348
|
+
async checkCodingExtensions(coding, context) {
|
|
349
|
+
const out = [];
|
|
350
|
+
const exts = coding.extension || [];
|
|
351
|
+
for (let index = 0; index < exts.length; index++) {
|
|
352
|
+
const ext = exts[index];
|
|
353
|
+
if (ext.url !== CLASSIFICATION_CODE_EXT) continue;
|
|
354
|
+
const value = ext.valueCode ?? '';
|
|
355
|
+
// the linearizations have classification codes; the Foundation has none
|
|
356
|
+
if (this.shared.systemCode === 'foundation') {
|
|
357
|
+
out.push({ index, msgId: 'ICD11_CLASSIFICATION_CODE_SYSTEM', params: [this.shared.url] });
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
const ctxt = await this.#ensureContext(context);
|
|
361
|
+
const id = await this.code(ctxt);
|
|
362
|
+
const expected = await this.classificationCode(ctxt);
|
|
363
|
+
if (coding.code !== id) {
|
|
364
|
+
out.push({ index, msgId: 'ICD11_CLASSIFICATION_CODE_NOT_ID', params: [coding.code, id] });
|
|
365
|
+
} else if (!expected) {
|
|
366
|
+
out.push({ index, msgId: 'ICD11_CLASSIFICATION_CODE_NONE', params: [value, coding.code] });
|
|
367
|
+
} else if (value !== expected) {
|
|
368
|
+
out.push({ index, msgId: 'ICD11_CLASSIFICATION_CODE_WRONG', params: [value, coding.code, expected] });
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return out;
|
|
372
|
+
}
|
|
373
|
+
|
|
263
374
|
async display(context) {
|
|
264
375
|
const ctxt = await this.#ensureContext(context);
|
|
265
376
|
if (ctxt === null) return null;
|
|
@@ -303,9 +414,9 @@ class ICD11Services extends BaseCSServices {
|
|
|
303
414
|
// narrower thing than any concept in the classification
|
|
304
415
|
if (ctxt === null || ctxt instanceof ICD11Expression) return null;
|
|
305
416
|
const row = await this.#get(
|
|
306
|
-
`SELECT c.
|
|
417
|
+
`SELECT c.entity_id, c.uri FROM concept_parent cp JOIN concept c ON c.id = cp.parent
|
|
307
418
|
WHERE cp.child = ? ORDER BY cp.seq LIMIT 1`, [ctxt.id]);
|
|
308
|
-
return row ? (row
|
|
419
|
+
return row ? this.#ref(row) : null;
|
|
309
420
|
}
|
|
310
421
|
|
|
311
422
|
/**
|
|
@@ -371,12 +482,15 @@ class ICD11Services extends BaseCSServices {
|
|
|
371
482
|
*/
|
|
372
483
|
propertyDefinitions() {
|
|
373
484
|
const defs = [
|
|
374
|
-
//
|
|
375
|
-
//
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
485
|
+
// The entity uri is the code (decided at the October 2026 test-case review); the short
|
|
486
|
+
// code is reported alongside it as classificationCode. Both are codes in this code
|
|
487
|
+
// system, so both are type code.
|
|
488
|
+
{ code: 'code', uri: `${SCHEMA_NS}/entityId`, type: 'code',
|
|
489
|
+
description: this.#bareIds
|
|
490
|
+
? 'The code: the WHO entity id. The entity uri is the system url + \'/\' + the id.'
|
|
491
|
+
: 'The code: the stable, unversioned WHO entity uri.' },
|
|
492
|
+
{ code: 'classificationCode', uri: `${SCHEMA_NS}/code`, type: 'code',
|
|
493
|
+
description: 'The short classification code (e.g. 1A00), where the entity has one.' },
|
|
380
494
|
{ code: 'classKind', uri: `${SCHEMA_NS}/classKind`, type: 'code',
|
|
381
495
|
description: 'chapter | block | window | category.' },
|
|
382
496
|
{ code: 'notSelectable', uri: 'http://hl7.org/fhir/concept-properties#notSelectable',
|
|
@@ -387,17 +501,31 @@ class ICD11Services extends BaseCSServices {
|
|
|
387
501
|
];
|
|
388
502
|
if (this.shared.systemCode !== 'foundation') {
|
|
389
503
|
defs.push(
|
|
390
|
-
{ code: 'source', uri: `${SCHEMA_NS}/source`, type: '
|
|
391
|
-
description: 'The Foundation entity this linearization entry came from
|
|
504
|
+
{ code: 'source', uri: `${SCHEMA_NS}/source`, type: 'Coding',
|
|
505
|
+
description: 'The Foundation entity this linearization entry came from, as a Coding in '
|
|
506
|
+
+ 'the Foundation. Requested as `source.details`, it also carries what a $lookup of '
|
|
507
|
+
+ 'that entity returns: subproperties (definition, parent) and designation parts.' },
|
|
392
508
|
{ code: 'blockId', uri: `${SCHEMA_NS}/blockId`, type: 'string' },
|
|
393
509
|
{ code: 'codeRange', uri: `${SCHEMA_NS}/codeRange`, type: 'string' },
|
|
394
|
-
{ code: '
|
|
395
|
-
description: '
|
|
510
|
+
{ code: 'postcoordination', uri: `${SCHEMA_NS}/postcoordination`, type: 'string',
|
|
511
|
+
description: 'One per axis the code declares, with subproperties axis, required, '
|
|
512
|
+
+ 'allowMultipleValues and valueSet; for an expression, also value (repeating), valued '
|
|
513
|
+
+ 'or not, and a value that is a cluster member carries its own postcoordination '
|
|
514
|
+
+ 'entries. Has no value of its own.' },
|
|
515
|
+
{ code: 'allowMultipleValues', uri: `${SCHEMA_NS}/allowMultipleValues`, type: 'Coding',
|
|
516
|
+
description: 'Subproperty of postcoordination: AllowAlways | NotAllowed | AllowedExceptFromSameBlock.' },
|
|
517
|
+
{ code: 'valueSet', uri: `${SCHEMA_NS}/valueSet`, type: 'string',
|
|
518
|
+
description: 'Subproperty of postcoordination: the canonical of the ValueSet of the values '
|
|
519
|
+
+ 'permitted on the axis.' },
|
|
396
520
|
// only ever returned for a postcoordinated expression, never for a plain concept
|
|
397
|
-
{ code: 'stem', uri: `${SCHEMA_NS}/stem`, type: '
|
|
398
|
-
description: '
|
|
399
|
-
{ code: '
|
|
400
|
-
description: '
|
|
521
|
+
{ code: 'stem', uri: `${SCHEMA_NS}/stem`, type: 'Coding',
|
|
522
|
+
description: 'The root of the expression.' },
|
|
523
|
+
{ code: 'axis', uri: `${SCHEMA_NS}/axis`, type: 'Coding',
|
|
524
|
+
description: 'Subproperty of postcoordination: the axis, in the schema vocabulary.' },
|
|
525
|
+
{ code: 'required', uri: `${SCHEMA_NS}/requiredPostcoordination`, type: 'boolean',
|
|
526
|
+
description: 'Subproperty of postcoordination: the axis must be given a value.' },
|
|
527
|
+
{ code: 'value', uri: `${SCHEMA_NS}/value`, type: 'Coding',
|
|
528
|
+
description: 'Subproperty of postcoordination: a code on the axis.' });
|
|
401
529
|
}
|
|
402
530
|
return defs;
|
|
403
531
|
}
|
|
@@ -406,11 +534,13 @@ class ICD11Services extends BaseCSServices {
|
|
|
406
534
|
const ctxt = await this.#ensureContext(context);
|
|
407
535
|
if (!ctxt) return [];
|
|
408
536
|
if (ctxt instanceof ICD11Expression) {
|
|
409
|
-
|
|
537
|
+
const out = [{ code: 'code', valueCode: ctxt.canonicalCode }];
|
|
538
|
+
if (ctxt.classificationCode) out.push({ code: 'classificationCode', valueCode: ctxt.classificationCode });
|
|
539
|
+
return out;
|
|
410
540
|
}
|
|
411
|
-
const out = [{ code: '
|
|
541
|
+
const out = [{ code: 'code', valueCode: ctxt.canonicalCode }];
|
|
412
542
|
if (ctxt.classKind) out.push({ code: 'classKind', valueCode: ctxt.classKind });
|
|
413
|
-
if (ctxt.code) out.push({ code: '
|
|
543
|
+
if (ctxt.code) out.push({ code: 'classificationCode', valueCode: ctxt.code });
|
|
414
544
|
if (!ctxt.selectable) out.push({ code: 'notSelectable', valueBoolean: true });
|
|
415
545
|
return out;
|
|
416
546
|
}
|
|
@@ -424,20 +554,25 @@ class ICD11Services extends BaseCSServices {
|
|
|
424
554
|
* the lot -- for 1A00 that is 7.5KB, mostly index terms -- which is one of the things
|
|
425
555
|
* the test suite pins.
|
|
426
556
|
*/
|
|
427
|
-
async extendLookup(ctxt, props, params) {
|
|
557
|
+
async extendLookup(ctxt, props, params, txParams) {
|
|
428
558
|
validateArrayParameter(props, 'props', String);
|
|
429
559
|
validateArrayParameter(params, 'params', Object);
|
|
430
560
|
const concept = await this.#ensureContext(ctxt);
|
|
431
561
|
if (!concept) return;
|
|
562
|
+
// post-coordination-metadata=true asks for every axis, with its rules; otherwise only the
|
|
563
|
+
// axes that carry a value are described, and only by what is on them
|
|
564
|
+
const meta = !!txParams?.postCoordinationMetadata;
|
|
432
565
|
if (concept instanceof ICD11Expression) {
|
|
433
|
-
return this.#extendLookupExpression(concept, props, params);
|
|
566
|
+
return this.#extendLookupExpression(concept, props, params, meta);
|
|
434
567
|
}
|
|
435
568
|
|
|
436
|
-
|
|
437
|
-
|
|
569
|
+
// code is the entity id (uri in the Foundation); classificationCode the short code,
|
|
570
|
+
// only where there is one
|
|
571
|
+
if (this._hasProp(props, 'code', true)) {
|
|
572
|
+
this._addCodeProperty(params, 'property', 'code', concept.canonicalCode);
|
|
438
573
|
}
|
|
439
|
-
if (concept.code && this._hasProp(props, '
|
|
440
|
-
this._addCodeProperty(params, 'property', '
|
|
574
|
+
if (concept.code && this._hasProp(props, 'classificationCode', true)) {
|
|
575
|
+
this._addCodeProperty(params, 'property', 'classificationCode', concept.code);
|
|
441
576
|
}
|
|
442
577
|
if (this._hasProp(props, 'classKind', true) && concept.classKind) {
|
|
443
578
|
this._addCodeProperty(params, 'property', 'classKind', concept.classKind);
|
|
@@ -454,94 +589,205 @@ class ICD11Services extends BaseCSServices {
|
|
|
454
589
|
if (concept.codeRange && this._hasProp(props, 'codeRange', true)) {
|
|
455
590
|
this._addStringProperty(params, 'property', 'codeRange', concept.codeRange);
|
|
456
591
|
}
|
|
457
|
-
|
|
458
|
-
|
|
592
|
+
// `source.details` is not a property of its own: it asks for source, with the Foundation
|
|
593
|
+
// entity's own lookup content inside it
|
|
594
|
+
const sourceDetails = props.some(p => String(p).toLowerCase() === 'source.details');
|
|
595
|
+
if (concept.sourceUri && (sourceDetails || this._hasProp(props, 'source', true))) {
|
|
596
|
+
params.push({ name: 'property', part: await this.#sourcePart(concept.sourceUri, sourceDetails) });
|
|
459
597
|
}
|
|
460
598
|
if (concept.browserUrl && this._hasProp(props, 'browserUrl', false)) {
|
|
461
599
|
this._addStringProperty(params, 'property', 'browserUrl', concept.browserUrl);
|
|
462
600
|
}
|
|
463
601
|
|
|
464
|
-
// hierarchy. Parents and children are codes in this code system, so they
|
|
465
|
-
//
|
|
602
|
+
// hierarchy. Parents and children are codes in this code system, so they are spelled
|
|
603
|
+
// the same way as the code.
|
|
466
604
|
if (this._hasProp(props, 'parent', true)) {
|
|
467
605
|
for (const r of await this.#all(
|
|
468
|
-
`SELECT c.
|
|
606
|
+
`SELECT c.entity_id, c.uri FROM concept_parent cp JOIN concept c ON c.id = cp.parent
|
|
469
607
|
WHERE cp.child = ? ORDER BY cp.seq`, [concept.id])) {
|
|
470
|
-
this._addCodeProperty(params, 'property', 'parent', r
|
|
608
|
+
this._addCodeProperty(params, 'property', 'parent', this.#ref(r));
|
|
471
609
|
}
|
|
472
610
|
}
|
|
473
611
|
if (this._hasProp(props, 'child', false)) {
|
|
474
612
|
for (const r of await this.#all(
|
|
475
|
-
`SELECT c.
|
|
613
|
+
`SELECT c.entity_id, c.uri FROM concept_parent cp JOIN concept c ON c.id = cp.child
|
|
476
614
|
WHERE cp.parent = ? ORDER BY cp.seq`, [concept.id])) {
|
|
477
|
-
this._addCodeProperty(params, 'property', 'child', r
|
|
615
|
+
this._addCodeProperty(params, 'property', 'child', this.#ref(r));
|
|
478
616
|
}
|
|
479
617
|
}
|
|
480
618
|
|
|
481
|
-
// The postcoordination axes this concept declares
|
|
482
|
-
//
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
619
|
+
// The postcoordination axes this concept declares: one postcoordination property per
|
|
620
|
+
// axis, in the shape a postcoordinated expression uses, with nothing on any axis. Only
|
|
621
|
+
// with post-coordination-metadata: without it only valued axes are listed, and a plain
|
|
622
|
+
// concept has none.
|
|
623
|
+
if (meta && this.shared.systemCode !== 'foundation' && this._hasProp(props, 'postcoordination', true)) {
|
|
624
|
+
for (const axis of await this.#all(
|
|
625
|
+
`SELECT id, seq, axis, axis_name, required, allow_multiple, value_set_uri FROM pc_scale
|
|
626
|
+
WHERE concept = ? ORDER BY seq`, [concept.id])) {
|
|
627
|
+
params.push({ name: 'property', part: this.#axisEntry(axis, true) });
|
|
487
628
|
}
|
|
488
629
|
}
|
|
489
630
|
}
|
|
490
631
|
|
|
491
632
|
/**
|
|
492
|
-
* $lookup for a postcoordinated expression
|
|
493
|
-
*
|
|
633
|
+
* $lookup for a postcoordinated expression (the October 2026 WHO meeting):
|
|
634
|
+
*
|
|
635
|
+
* code, classificationCode the id form and the short-code form, as for a plain concept
|
|
636
|
+
* stem the root, as a Coding
|
|
637
|
+
* postcoordination one per axis the root declares, valued or not:
|
|
638
|
+
* axis Coding in the schema vocabulary
|
|
639
|
+
* required boolean
|
|
640
|
+
* allowMultipleValues Coding (AllowAlways | NotAllowed | ...)
|
|
641
|
+
* valueSet string: the canonical of the axis's ValueSet
|
|
642
|
+
* value Coding, repeating: what is on the axis
|
|
643
|
+
* plus one with only `value`s, for `/` members on no axis of the
|
|
644
|
+
* root.
|
|
645
|
+
*
|
|
646
|
+
* Without post-coordination-metadata=true only the axes that carry a value are listed, and
|
|
647
|
+
* each carries only `axis` and its values: required, allowMultipleValues and valueSet, and
|
|
648
|
+
* the unvalued axes, are the metadata. (A `/` member with no qualifiers is then just its
|
|
649
|
+
* Coding.)
|
|
494
650
|
*
|
|
495
|
-
*
|
|
496
|
-
*
|
|
651
|
+
* A `value` that is a `/` member is a code with axes of its own, and carries its own
|
|
652
|
+
* postcoordination entries inside it, one level down - the same shape. That nesting is the
|
|
653
|
+
* only way to keep a member's qualifiers with that member: the same code can be a member
|
|
654
|
+
* twice with different qualifiers. ($lookup does not describe subproperties inside
|
|
655
|
+
* subproperties; Parameters allows them.)
|
|
497
656
|
*
|
|
498
|
-
*
|
|
499
|
-
*
|
|
500
|
-
* client that only has the string cannot tell, and a server that reports both on the
|
|
501
|
-
* first axis has told it something untrue. Each (axis, value) pair is therefore kept
|
|
502
|
-
* distinct, and the axis is named by its schema uri, the same identifier the
|
|
503
|
-
* postcoordinationScale property uses on a stem.
|
|
657
|
+
* Everything comes out in normal-form order: axes in the order the code defines them,
|
|
658
|
+
* values in the order the normal form puts them.
|
|
504
659
|
*/
|
|
505
|
-
async #extendLookupExpression(expr, props, params) {
|
|
660
|
+
async #extendLookupExpression(expr, props, params, meta) {
|
|
506
661
|
if (this._hasProp(props, 'code', true)) {
|
|
507
662
|
this._addCodeProperty(params, 'property', 'code', expr.canonicalCode);
|
|
508
663
|
}
|
|
509
|
-
if (this._hasProp(props, '
|
|
510
|
-
this._addCodeProperty(params, 'property', '
|
|
664
|
+
if (expr.classificationCode && this._hasProp(props, 'classificationCode', true)) {
|
|
665
|
+
this._addCodeProperty(params, 'property', 'classificationCode', expr.classificationCode);
|
|
511
666
|
}
|
|
667
|
+
const root = expr.stems[0];
|
|
512
668
|
if (this._hasProp(props, 'stem', true)) {
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
{ name: 'value', valueString: label }] });
|
|
520
|
-
}
|
|
521
|
-
part.push({ name: 'subproperty', part: [
|
|
522
|
-
{ name: 'code', valueCode: 'stemUri' },
|
|
523
|
-
{ name: 'value', valueUri: stem.stem.concept.uri }] });
|
|
524
|
-
part.push({ name: 'value', valueCode: stem.stem.concept.canonicalCode });
|
|
669
|
+
params.push({ name: 'property', part: [
|
|
670
|
+
{ name: 'code', valueCode: 'stem' },
|
|
671
|
+
{ name: 'value', valueCoding: await this.#coding(root.stem.concept) }] });
|
|
672
|
+
}
|
|
673
|
+
if (this._hasProp(props, 'postcoordination', true)) {
|
|
674
|
+
for (const part of await this.#postcoordination(expr, root, true, meta)) {
|
|
525
675
|
params.push({ name: 'property', part });
|
|
526
676
|
}
|
|
527
677
|
}
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* The `source` property: the Foundation entity a linearization entry came from, as a Coding
|
|
682
|
+
* in the Foundation (whose codes are entity uris), with its title. With `details`, it also
|
|
683
|
+
* carries what a $lookup of that entity would say: as subproperties, its definition and its
|
|
684
|
+
* parents in the Foundation (codes there, as $lookup returns them); and its designations, as
|
|
685
|
+
* `designation` parts shaped like a $lookup's own. The display is already on the Coding.
|
|
686
|
+
* The Foundation is in the same database, so no second provider is needed; if a
|
|
687
|
+
* database was built without it, the Coding is all there is.
|
|
688
|
+
*/
|
|
689
|
+
async #sourcePart(uri, details) {
|
|
690
|
+
const row = await this.#get(
|
|
691
|
+
`SELECT c.id, s.url AS system_url FROM concept c JOIN system s ON s.id = c.system
|
|
692
|
+
WHERE s.code = 'foundation' AND c.uri = ?`, [uri]);
|
|
693
|
+
const coding = { system: row ? row.system_url : FOUNDATION_URL, code: uri };
|
|
694
|
+
const label = row ? await this.#title(row) : null;
|
|
695
|
+
if (label) coding.display = label;
|
|
696
|
+
const part = [{ name: 'code', valueCode: 'source' }, { name: 'value', valueCoding: coding }];
|
|
697
|
+
if (details && row) {
|
|
698
|
+
const sub = (code, value) => ({ name: 'subproperty', part: [{ name: 'code', valueCode: code }, { name: 'value', ...value }] });
|
|
699
|
+
const def = await this.#get(
|
|
700
|
+
`SELECT value FROM designation
|
|
701
|
+
WHERE concept = ? AND kind = 'definition' AND lang = ? ORDER BY seq LIMIT 1`,
|
|
702
|
+
[row.id, this.#displayLang()]);
|
|
703
|
+
if (def) part.push(sub('definition', { valueString: def.value }));
|
|
704
|
+
for (const r of await this.#all(
|
|
705
|
+
`SELECT c.uri FROM concept_parent cp JOIN concept c ON c.id = cp.parent
|
|
706
|
+
WHERE cp.child = ? ORDER BY cp.seq`, [row.id])) {
|
|
707
|
+
part.push(sub('parent', { valueCode: r.uri }));
|
|
708
|
+
}
|
|
709
|
+
// and its designations, in the same shape as a $lookup's own - which the operation does
|
|
710
|
+
// not describe inside a property, but they are what a $lookup of the entity would say,
|
|
711
|
+
// and a subproperty (a code and a value) cannot carry their language and use
|
|
712
|
+
for (const r of await this.#all(
|
|
713
|
+
`SELECT lang, kind, value FROM designation WHERE concept = ? ORDER BY lang, seq`, [row.id])) {
|
|
714
|
+
if (NOT_A_DESIGNATION.has(r.kind)) continue;
|
|
715
|
+
const value = r.value.trim();
|
|
716
|
+
if (!value) continue;
|
|
717
|
+
const use = r.kind === 'title' ? { ...DesignationUse.DISPLAY } : { system: SCHEMA_NS, code: r.kind };
|
|
718
|
+
part.push({ name: 'designation', part: [
|
|
719
|
+
{ name: 'language', valueCode: r.lang },
|
|
720
|
+
{ name: 'use', valueCoding: use },
|
|
721
|
+
{ name: 'value', valueString: value }] });
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
return part;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/** A concept in this code system as a Coding, with its display. */
|
|
728
|
+
async #coding(concept) {
|
|
729
|
+
const c = { system: this.shared.url, code: concept.canonicalCode };
|
|
730
|
+
const label = await this.#title(concept);
|
|
731
|
+
if (label) c.display = label;
|
|
732
|
+
return c;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* The postcoordination entries for one code of an expression - the root, or a `/` member -
|
|
737
|
+
* each as the `part` list of a property (or, one level down, of a subproperty).
|
|
738
|
+
*/
|
|
739
|
+
async #postcoordination(expr, st, isRoot, meta) {
|
|
740
|
+
const pos = (t) => expr.tokens.indexOf(t);
|
|
741
|
+
const out = [];
|
|
742
|
+
for (const axis of [...st.axes].sort((a, b) => a.seq - b.seq)) {
|
|
743
|
+
const values = st.bindings.filter(b => b.axis === axis).map(b => b.token).sort((a, b) => pos(a) - pos(b));
|
|
744
|
+
if (!meta && values.length === 0) continue; // unvalued axes only with the metadata
|
|
745
|
+
const part = this.#axisEntry(axis, meta);
|
|
746
|
+
for (const t of values) part.push(await this.#valuePart(expr, t, meta));
|
|
747
|
+
out.push(part);
|
|
748
|
+
}
|
|
749
|
+
if (isRoot) {
|
|
750
|
+
// `/` members that are on no axis of the root still qualify it
|
|
751
|
+
const loose = expr.tokens.filter(t => t.sep === '/' && !t.axis);
|
|
752
|
+
if (loose.length) {
|
|
753
|
+
const part = [{ name: 'code', valueCode: 'postcoordination' }];
|
|
754
|
+
for (const t of loose) part.push(await this.#valuePart(expr, t, meta));
|
|
755
|
+
out.push(part);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
return out;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* The start of a postcoordination entry: the axis and what is known about it. A plain
|
|
763
|
+
* concept's lookup stops here; an expression's adds the values.
|
|
764
|
+
*/
|
|
765
|
+
#axisEntry(axis, meta) {
|
|
766
|
+
const sub = (code, value) => ({ name: 'subproperty', part: [{ name: 'code', valueCode: code }, { name: 'value', ...value }] });
|
|
767
|
+
const part = [
|
|
768
|
+
{ name: 'code', valueCode: 'postcoordination' },
|
|
769
|
+
sub('axis', { valueCoding: { system: SCHEMA_NS, code: axis.axis_name, display: axisLabel(axis.axis_name) } })];
|
|
770
|
+
if (!meta) return part; // the axis rules are post-coordination-metadata
|
|
771
|
+
part.push(sub('required', { valueBoolean: !!axis.required }));
|
|
772
|
+
if (axis.allow_multiple) {
|
|
773
|
+
part.push(sub('allowMultipleValues', { valueCoding: { system: ALLOW_MULTIPLE_CS, code: axis.allow_multiple } }));
|
|
774
|
+
}
|
|
775
|
+
if (axis.value_set_uri) {
|
|
776
|
+
part.push(sub('valueSet', { valueString: axis.value_set_uri }));
|
|
777
|
+
}
|
|
778
|
+
return part;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/** One `value` subproperty; a `/` member carries its own axes inside it. */
|
|
782
|
+
async #valuePart(expr, token, meta) {
|
|
783
|
+
const part = [{ name: 'code', valueCode: 'value' }, { name: 'value', valueCoding: await this.#coding(token.concept) }];
|
|
784
|
+
const member = token.sep === '/' ? expr.stems.find(st => st.stem === token) : null;
|
|
785
|
+
if (member) {
|
|
786
|
+
for (const p of await this.#postcoordination(expr, member, false, meta)) {
|
|
787
|
+
part.push({ name: 'subproperty', part: p });
|
|
543
788
|
}
|
|
544
789
|
}
|
|
790
|
+
return { name: 'subproperty', part };
|
|
545
791
|
}
|
|
546
792
|
|
|
547
793
|
// -- subsumption -----------------------------------------------------------------------
|
|
@@ -591,15 +837,15 @@ class ICD11Services extends BaseCSServices {
|
|
|
591
837
|
|
|
592
838
|
async nextContext(iteratorContext) {
|
|
593
839
|
if (!iteratorContext || iteratorContext.cursor >= iteratorContext.rows.length) return null;
|
|
594
|
-
return
|
|
840
|
+
return this.#concept(iteratorContext.rows[iteratorContext.cursor++]);
|
|
595
841
|
}
|
|
596
842
|
|
|
597
843
|
// -- filters ---------------------------------------------------------------------------
|
|
598
844
|
|
|
599
845
|
/**
|
|
600
846
|
* `concept is-a` and `concept descendent-of` are what WHO's own value sets use, and what
|
|
601
|
-
* a postcoordination scale is expressed with. The filter value
|
|
602
|
-
*
|
|
847
|
+
* a postcoordination scale is expressed with. The filter value is a code as #resolve
|
|
848
|
+
* reads it: the short code, or the entity id (MMS, ICF) or uri (Foundation).
|
|
603
849
|
*/
|
|
604
850
|
async doesFilter(prop, op, value) {
|
|
605
851
|
return prop === 'concept' && (op === 'is-a' || op === 'descendent-of') && !!value;
|
|
@@ -653,13 +899,18 @@ class ICD11Services extends BaseCSServices {
|
|
|
653
899
|
WHERE cl.ancestor = ? AND cl.depth >= ?
|
|
654
900
|
ORDER BY COALESCE(c.lft, c.id)`, [set.ancestorId, set.minDepth]);
|
|
655
901
|
} else {
|
|
902
|
+
// the text, or the start of a short code (searching 1A0 finds 1A00..1A09): the code
|
|
903
|
+
// is a uri now, so the short code is the one a person types
|
|
656
904
|
set.rows = await this.#all(
|
|
657
|
-
`SELECT
|
|
658
|
-
WHERE c.system = ?
|
|
659
|
-
AND
|
|
660
|
-
|
|
905
|
+
`SELECT c.* FROM concept c
|
|
906
|
+
WHERE c.system = ?
|
|
907
|
+
AND (LOWER(c.code) LIKE ?
|
|
908
|
+
OR EXISTS (SELECT 1 FROM designation d
|
|
909
|
+
WHERE d.concept = c.id AND d.lang IN (?, ?)
|
|
910
|
+
AND d.kind IN ('title', 'indexTerm', 'inclusion', 'fullySpecifiedName')
|
|
911
|
+
AND LOWER(d.value) LIKE ?))
|
|
661
912
|
ORDER BY COALESCE(c.lft, c.id)`,
|
|
662
|
-
[this.shared.systemId, this.#displayLang(), this.defLang(), `%${set.text}%`]);
|
|
913
|
+
[this.shared.systemId, `${set.text}%`, this.#displayLang(), this.defLang(), `%${set.text}%`]);
|
|
663
914
|
}
|
|
664
915
|
return set.rows;
|
|
665
916
|
}
|
|
@@ -675,7 +926,7 @@ class ICD11Services extends BaseCSServices {
|
|
|
675
926
|
async filterConcept(filterContext, set) {
|
|
676
927
|
const rows = await this.#materialise(set);
|
|
677
928
|
if (set.cursor >= rows.length) return null;
|
|
678
|
-
return
|
|
929
|
+
return this.#concept(rows[set.cursor++]);
|
|
679
930
|
}
|
|
680
931
|
|
|
681
932
|
async filterLocate(filterContext, set, code) {
|
|
@@ -694,11 +945,11 @@ class ICD11Services extends BaseCSServices {
|
|
|
694
945
|
`SELECT 1 AS ok FROM concept_closure
|
|
695
946
|
WHERE ancestor = ? AND descendant = ? AND depth >= ? LIMIT 1`,
|
|
696
947
|
[set.ancestorId, concept.id, set.minDepth]);
|
|
697
|
-
return row ? true : `'${concept.canonicalCode}' is not in the value set`;
|
|
948
|
+
return row ? true : `'${concept.code || concept.canonicalCode}' is not in the value set`;
|
|
698
949
|
}
|
|
699
950
|
const rows = await this.#materialise(set);
|
|
700
951
|
return rows.some(r => r.id === concept.id)
|
|
701
|
-
? true : `'${concept.canonicalCode}' is not in the value set`;
|
|
952
|
+
? true : `'${concept.code || concept.canonicalCode}' is not in the value set`;
|
|
702
953
|
}
|
|
703
954
|
|
|
704
955
|
/** The filter sets are closed, whatever the code system as a whole may be. */
|
|
@@ -791,7 +1042,7 @@ class ICD11ServicesFactory extends CodeSystemFactoryProvider {
|
|
|
791
1042
|
|
|
792
1043
|
/**
|
|
793
1044
|
* The postcoordination scale value sets. WHO publishes one per axis per stem -- 25,354 of
|
|
794
|
-
* them for 2026-01 -- and $lookup hands out their canonicals as the
|
|
1045
|
+
* them for 2026-01 -- and $lookup hands out their canonicals as the valueSet of each postcoordination
|
|
795
1046
|
* property of a concept, so a client that follows one has to find something at the other
|
|
796
1047
|
* end. They are built on demand from pc_scale rather than shipped: the compose is three
|
|
797
1048
|
* or four is-a includes, and pre-expanding the lot would be 14.6 million rows.
|
|
@@ -811,8 +1062,11 @@ class ICD11ServicesFactory extends CodeSystemFactoryProvider {
|
|
|
811
1062
|
FROM pc_scale ps JOIN concept c ON c.id = ps.concept
|
|
812
1063
|
WHERE ps.value_set_uri = ? AND c.system = ?`, [url, this._shared.systemId]);
|
|
813
1064
|
if (!scale) return null;
|
|
1065
|
+
// the filter values are codes, so ids: the scale root's uri is system + '/' + its id
|
|
814
1066
|
const roots = await all(
|
|
815
1067
|
`SELECT target_uri FROM pc_scale_entity WHERE scale = ? ORDER BY seq`, [scale.id]);
|
|
1068
|
+
const prefix = `${this._shared.url}/`;
|
|
1069
|
+
const rootCode = (u) => (u.startsWith(prefix) ? u.slice(prefix.length) : u);
|
|
816
1070
|
|
|
817
1071
|
const slug = `${this._shared.systemCode}-${scale.entity_id}-${scale.axis_name}`.replace(/\//g, '-');
|
|
818
1072
|
return {
|
|
@@ -827,14 +1081,14 @@ class ICD11ServicesFactory extends CodeSystemFactoryProvider {
|
|
|
827
1081
|
date: this._shared.version,
|
|
828
1082
|
publisher: 'World Health Organization',
|
|
829
1083
|
description: `The values WHO permits on the ${scale.axis_name} postcoordination axis `
|
|
830
|
-
+ `of ${scale.stem_code || scale.
|
|
1084
|
+
+ `of ${scale.stem_code || scale.entity_id}.`,
|
|
831
1085
|
compose: {
|
|
832
1086
|
// one include per scale root: membership is the root and everything under it,
|
|
833
1087
|
// which is exactly how WHO expresses it in its own published value sets
|
|
834
1088
|
include: roots.map(r => ({
|
|
835
1089
|
system: this._shared.url,
|
|
836
1090
|
version: this._shared.version,
|
|
837
|
-
filter: [{ property: 'concept', op: 'is-a', value: r.target_uri }],
|
|
1091
|
+
filter: [{ property: 'concept', op: 'is-a', value: rootCode(r.target_uri) }],
|
|
838
1092
|
})),
|
|
839
1093
|
},
|
|
840
1094
|
};
|